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 tracer API #11

Merged
merged 13 commits into from
Jun 20, 2019
Empty file added opentelemetry/__init__.py
Empty file.
Empty file added opentelemetry/api/__init__.py
Empty file.
241 changes: 241 additions & 0 deletions opentelemetry/api/trace/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# 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.
Copy link
Member Author

Choose a reason for hiding this comment

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

These docs are WIP, I just wanted a good example module docstring here to test the generated html docs.


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::

from opentelemetry.sdk.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.sdk.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.
Copy link
Member

Choose a reason for hiding this comment

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

How about libraries? The application could use explicit propagation consistently, while calling into a library which expects implicit context propagation.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, we definitely should think about and document best practices for what libs should do. For that question, see https://github.com/open-telemetry/opentelemetry-python/pull/11/files#r290703075. Another question is what tracer they should use (e.g.: Use global tracer? Pass tracer at every call? Provide way to set library-wide tracer? Provide way to set a library-wide callback function that it calls to get the tracer?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Best-practices aside, users should have the final thing to say - if they, for some reason, need to 'mix' these modes (as you guys call it), that should be supported IHMO.

(As for the other questions, I'd say using a global Tracer would be a good thing to have)

Copy link
Member Author

Choose a reason for hiding this comment

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

The application could use explicit propagation consistently, while calling into a library which expects implicit context propagation.

This is a problem since it would mean that passing an explicit context should change the implicit one. This would be unexpected in the application, but required if it's calling into a library that uses implicit context.

Provide way to set a library-wide callback function that it calls to get the tracer?

I thought implementations would provide a global tracer, do you want the callback so integrations can change this?


.. 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`.
"""
raise NotImplementedError


@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.span("one") as parent:
parent.add_event("parent's event")
with tracer.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):
Copy link
Member

@reyang reyang Jun 6, 2019

Choose a reason for hiding this comment

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

Maybe I've missed something, I was thinking a different approach:

Implicit
with tracer.span(name):
    do_work()
Explicit
span = tracer.span(name)
span.start()
do_work()
span.finish()

And in the span class we will have __enter__ and __exit__ (or __aenter__ and __aexit__).

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 a nice interface, but what do you do about the fact that we need to modify the context if span is used as a context manager and not otherwise?

do_work()

Args:
name: The name of the span to be created.
parent: The span's parent.

Yields:
The newly-created span.
"""
raise NotImplementedError

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.
"""
raise NotImplementedError

@contextmanager
def use_span(self, span: Span) -> Iterator[None]:
Copy link
Member

Choose a reason for hiding this comment

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

For languages which don't have the with or using concept, it might be good to have a dedicated API. For Python, with span (Span.__enter__ and Span.__exit__) could be a good replacement for use_span?

Copy link
Member

Choose a reason for hiding this comment

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

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 agree if we can find a nice way to handle modifying the context.

"""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.
"""
raise NotImplementedError


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.
"""
raise NotImplementedError

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.
"""
raise NotImplementedError

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.
"""
raise NotImplementedError


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