Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial DistributedContext implementation. #92

Merged
merged 23 commits into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that currently only NO_PROPAGATION and UNLIMITED_PROPAGATION are allowed should be a check for that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure there's much value in doing a runtime check for these values, especially considering the intended purpose is to actually support positive integers in the future.



class EntryKey(str):
a-feld marked this conversation as resolved.
Show resolved Hide resolved
a-feld marked this conversation as resolved.
Show resolved Hide resolved
"""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)
a-feld marked this conversation as resolved.
Show resolved Hide resolved

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)
Oberon00 marked this conversation as resolved.
Show resolved Hide resolved


class Entry:
def __init__(
self,
metadata: EntryMetadata,
key: EntryKey,
value: EntryValue,
) -> None:
self.metadata = metadata
self.key = key
self.value = value


class DistributedContext:
a-feld marked this conversation as resolved.
Show resolved Hide resolved
"""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
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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.

"""
Original file line number Diff line number Diff line change
@@ -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.

"""
13 changes: 13 additions & 0 deletions opentelemetry-api/tests/distributedcontext/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Loading