diff --git a/docs/opentelemetry.trace.rst b/docs/opentelemetry.trace.rst index cec44bd8178..a57b5dcbff8 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 00000000000..0205446c808 --- /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/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index f1e37aa91f4..99d6869dd52 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-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index e9992165b57..d2426fd31d1 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. @@ -239,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/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py new file mode 100644 index 00000000000..4fc50b33e56 --- /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-api/tests/distributedcontext/test_distributed_context.py b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py index 67a60048399..c730603b162 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 14667f62eaa..97ac92fcdef 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 942479ab7dc..970b6159630 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/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 461203ede65..d33aa01146c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -144,6 +144,7 @@ def __init__( self.kind = kind self.span_processor = span_processor + self.status = trace_api.Status() self._lock = threading.Lock() if attributes is None: @@ -286,6 +287,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/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index a76a658b3a3..ecdc93b0adb 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): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 32e227c7755..c167e374aca 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -312,6 +312,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)) @@ -358,6 +376,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" diff --git a/tox.ini b/tox.ini index 0db2364f197..19816d3d9c1 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/