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

Span creation in tracer SDK #69

Merged
merged 12 commits into from
Aug 6, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
1 change: 0 additions & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
[settings]
force_single_line=True
from_first=True
from_first=True
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions opentelemetry-api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import os

import setuptools

BASE_DIR = os.path.dirname(__file__)
Expand Down
42 changes: 34 additions & 8 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@

from opentelemetry import loader

# TODO: quarantine
ParentSpan = typing.Optional[typing.Union['Span', 'SpanContext']]
c24t marked this conversation as resolved.
Show resolved Hide resolved


class Span:
"""A span represents a single operation within a trace."""
Expand Down Expand Up @@ -141,6 +144,14 @@ def get_default(cls) -> 'TraceState':
DEFAULT_TRACESTATE = 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.

Expand All @@ -157,12 +168,25 @@ class SpanContext:
def __init__(self,
trace_id: int,
span_id: int,
options: 'TraceOptions',
state: 'TraceState') -> None:
traceoptions: 'TraceOptions' = None,
tracestate: 'TraceState' = None
) -> None:
if traceoptions is None:
Copy link
Member

Choose a reason for hiding this comment

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

According to the current specification, we need to have traceOptions.

Copy link
Member

Choose a reason for hiding this comment

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

Or trace_options, either way looks weird though.

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 don't think the spec is meant to dictate internal names like this, especially since it's clear what this field represents. In any case we'll follow the W3C spec when the class gets serialized.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's more about "do we treat traceoptions as a single word or not" rather than "do we take the name strictly from the spec".
The W3C spec is using two words, having traceoptions is against Python naming convention.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's fair. I prefer traceoptions/state but agree it's more consistent to split the words up. Changed in 0f52732.

traceoptions = DEFAULT_TRACEOPTIONS
if tracestate is None:
tracestate = DEFAULT_TRACESTATE
self.trace_id = trace_id
self.span_id = span_id
self.options = options
self.state = state
self.traceoptions = traceoptions
self.tracestate = tracestate

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

def is_valid(self) -> bool:
"""Get whether this `SpanContext` is valid.
Expand All @@ -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):
Expand All @@ -187,8 +213,8 @@ 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)
INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
19 changes: 19 additions & 0 deletions opentelemetry-api/src/opentelemetry/types.py
Original file line number Diff line number Diff line change
@@ -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]
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Attributes = typing.Dict[str, AttributeValue]
1 change: 1 addition & 0 deletions opentelemetry-sdk/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import os

import setuptools

BASE_DIR = os.path.dirname(__file__)
Expand Down
159 changes: 129 additions & 30 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -144,20 +149,29 @@ 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 generally create `Span`s via the `Tracer` instead of this
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should highlight this in a stronger tone - I'd really disliked users creating Spans directly (unless you guys have a specific reason/case in mind).

Copy link
Member Author

Choose a reason for hiding this comment

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

No specific use case, how about removing "generally"?

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)
Expand All @@ -166,27 +180,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
Expand All @@ -213,25 +219,35 @@ 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)
self.attributes[key] = value

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)
Expand All @@ -246,5 +262,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)
Copy link
Member

Choose a reason for hiding this comment

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

We might need to move this into API if we plan to support the "default behavior" in https://github.com/w3c/trace-context/blob/master/spec/20-http_header_format.md#mutating-the-traceparent-field.
We can explore this later, no need to block this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Besides what @reyang commented: I remember we kept this private in Java, to not have use this as a defacto standard for creating span ids. Something to consider?

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 don't have a strong opinion here, and think the bar for making things private is generally a lot lower in java than python. But since we don't have a convention for marking the SDK's "API", hiding everything we don't expect users to call may be a good idea.

That said, if someone is calling into the SDK package instead of just using the API they're already voiding the warranty...



def generate_trace_id():
Copy link
Contributor

Choose a reason for hiding this comment

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

(Same as generate_span_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
Copy link
Contributor

Choose a reason for hiding this comment

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

What about using the context-propagation layer? Will that happen in the future?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'll do that in another PR.

) -> 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.traceoptions,
parent_context.tracestate)
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
1 change: 1 addition & 0 deletions opentelemetry-sdk/tests/resources/test_init.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest

from opentelemetry.sdk import resources


Expand Down
Loading