Skip to content

Commit

Permalink
Merge pull request #362 from tsloughter/api-readme
Browse files Browse the repository at this point in the history
Add docs for the tracing api macros to the readme
  • Loading branch information
Tristan Sloughter authored Feb 22, 2022
2 parents 01e4944 + 7e45530 commit 2574d64
Show file tree
Hide file tree
Showing 16 changed files with 296 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Compile
run: rebar3 as test compile
- name: ExUnit
run: mix test test/otel_tests.exs
run: mix test --no-start test/otel_tests.exs

api_tests:
runs-on: ${{ matrix.os }}
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- [Simpler configuration of span processors](https://github.com/open-telemetry/opentelemetry-erlang/pull/357)

#### Fixed

- Span Status: Ignore status changes that don't follow the [define precedence in
the spec](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#set-status)

### [Zipkin Exporter]

#### Fixed
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ slack](https://slack.cncf.io/). Please join us for more informal discussions.
You can also find us in the #opentelemetry channel on [Elixir
Slack](https://elixir-slackin.herokuapp.com/).

## Getting Started

You can find a getting started guide on [opentelemetry.io](https://opentelemetry.io/docs/instrumentation/erlang/getting-started/).

To start capturing distributed traces from your application it first needs to be
instrumented. The easiest way to do this is by using an instrumentation library,
there are a number of [officially supported instrumentation
libraries](https://github.com/open-telemetry/opentelemetry-erlang-contrib) for
popular Erlang and Elixir libraries and frameworks.

## Design

The [OpenTelemetry
Expand Down
21 changes: 19 additions & 2 deletions apps/opentelemetry/src/otel_span_ets.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
-include_lib("opentelemetry_api/include/opentelemetry.hrl").
-include("otel_span.hrl").
-include("otel_span_ets.hrl").
-include_lib("stdlib/include/ms_transform.hrl").

-record(state, {}).

Expand Down Expand Up @@ -136,8 +137,24 @@ add_events(#span_ctx{span_id=SpanId}, NewEvents) ->
end.

-spec set_status(opentelemetry:span_ctx(), opentelemetry:status()) -> boolean().
set_status(#span_ctx{span_id=SpanId}, Status) ->
ets:update_element(?SPAN_TAB, SpanId, {#span.status, Status}).
set_status(#span_ctx{span_id=SpanId}, Status=#status{code=NewCode}) ->
MS = ets:fun2ms(fun(Span=#span{span_id=Id,
status=#status{code=?OTEL_STATUS_ERROR}}) when Id =:= SpanId ,
NewCode =:= ?OTEL_STATUS_OK ->
%% can only set status to OK if it has been set to ERROR before
Span#span{status=#status{code=?OTEL_STATUS_OK}};
(Span=#span{span_id=Id,
status=#status{code=?OTEL_STATUS_UNSET}}) when Id =:= SpanId ->
%% if UNSET then the status can be updated to OK or ERROR
Span#span{status=Status};
(Span=#span{span_id=Id,
status=undefined}) when Id =:= SpanId ->
%% if undefined then the status can be updated to anything
Span#span{status=Status}
end),
ets:select_replace(?SPAN_TAB, MS) =:= 1;
set_status(_, _) ->
false.

-spec update_name(opentelemetry:span_ctx(), opentelemetry:span_name()) -> boolean().
update_name(#span_ctx{span_id=SpanId}, Name) ->
Expand Down
28 changes: 23 additions & 5 deletions apps/opentelemetry/test/opentelemetry_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -373,15 +373,33 @@ update_span_data(Config) ->
tracestate=[]}],

SpanCtx1=#span_ctx{trace_id=TraceId,
span_id=SpanId} = ?start_span(<<"span-1">>, #{links => Links}),
span_id=SpanId,
is_recording=true} = ?start_span(<<"span-1">>, #{links => Links}),
?set_current_span(SpanCtx1),
?set_attribute(<<"key-1">>, <<"value-1">>),

Events = opentelemetry:events([{opentelemetry:timestamp(),
<<"event-name">>, []}]),
Status = opentelemetry:status(0, <<"status">>),

otel_span:set_status(SpanCtx1, Status),
ErrorStatus = opentelemetry:status(?OTEL_STATUS_ERROR, <<"status">>),
?assertMatch(#status{code=?OTEL_STATUS_ERROR,
message = <<"status">>}, ErrorStatus),

OkStatus = opentelemetry:status(?OTEL_STATUS_OK, <<"will be ignored">>),
?assertMatch(#status{code=?OTEL_STATUS_OK,
message = <<>>}, OkStatus),
?assertEqual(OkStatus, opentelemetry:status(?OTEL_STATUS_OK)),

UnsetStatus = opentelemetry:status(?OTEL_STATUS_UNSET, <<"will be ignored">>),
?assertMatch(#status{code=?OTEL_STATUS_UNSET,
message = <<>>}, UnsetStatus),
?assertEqual(UnsetStatus, opentelemetry:status(?OTEL_STATUS_UNSET)),

?assert(otel_span:set_status(SpanCtx1, OkStatus)),
%% spec does not allow setting status to error/unset after it is ok
?assertNot(otel_span:set_status(SpanCtx1, ErrorStatus)),
?assertNot(otel_span:set_status(SpanCtx1, ?OTEL_STATUS_ERROR)),
%% %% returns false if called with something that isn't a status record
?assertNot(otel_span:set_status(SpanCtx1, notastatus)),

%% returning not false means it successfully called the SDK
?assertNotEqual(false, otel_span:add_event(SpanCtx1, event_1, #{<<"attr-1">> => <<"attr-value-1">>})),
Expand All @@ -394,7 +412,7 @@ update_span_data(Config) ->
links=L,
events=E}] = ?UNTIL_NOT_EQUAL([], ets:match_object(Tid, #span{trace_id=TraceId,
span_id=SpanId,
status=Status,
status=OkStatus,
_='_'})),


Expand Down
169 changes: 166 additions & 3 deletions apps/opentelemetry_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,180 @@ some_fun() ->

``` elixir
require OpenTelemetry.Tracer
require OpenTelemetry.Span

def some_fun() do
OpenTelemetry.Tracer.with_span "some-span" do
...
OpenTelemetry.Span.set_attribute("key", "value")
OpenTelemetry.Tracer.set_attribute("key", "value")
...
end
end
```

### Tracing API

The macros and functions available for Elixir in `OpenTelemetry.Tracer` and the
Erlang macros in `otel_tracer.hrl` are the best way to work with Spans. They
will automatically use the Tracer named for the Application the module using the
macro is in. For example, the Spans created in
[opentelemetry_oban](https://hex.pm/packages/opentelemetry_oban) use the
`with_span` macro resulting in the Span being created with the
`opentelemetry_oban` named Tracer and associated with the [Instrumentation
Library](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/glossary.md#instrumentation-library)
of the same name and version of the Tracer -- the version also matches the
`opentelemetry_oban` Application version.

#### Context

[Context](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/context/context.md) is used to pass values associated with the current [execution
unit](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/glossary.md#execution-unit).
At this time the only values kept in the Context by this OpenTelemetry library
are the [Span
Context](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#spancontext)
for the currently active Span and the
[Baggage](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/baggage/api.md)

When a Context variable is not an explicit argument in the API macros or
functions the Context from the [process
dictionary](https://www.erlang.org/doc/reference_manual/processes.html#process-dictionary)
is used. If no Context is found in the current process's pdict then one is
created.

#### Starting and Ending Spans

A Span represents a single operation in a Trace. It has a start and end time,
can have a single parent and one or more children. The easiest way to create
Spans is to wrap the operation you want a Span to represent in the `with_span`
macro. The macro handles getting a
[Tracer](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#tracer)
associated with the OTP Application the module is in, starting the Span, setting
it as the currently active Span in the Context stored in the process dictionary
and ending the Span when the `Fun` or body of the Elixir macro finish, even if
an exception is thrown -- however, the exception is not caught, so it does not
change how user code should deal with raised exceptions. After the Span is
ended the Context in the process dictionary is reset to its value before the
newly started Span was set as the active Span. This handling of the active Span
in the process dictionary ensures proper lineage of Spans is kept when starting
and ending child Spans.

``` erlang
?with_span(SpanName, StartOpts, Fun)
```

``` elixir
OpenTelemetry.Tracer.with_span name, start_opts do
...
end
```

`StartOpts`/`start_opts` is a map of [Span creation options](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#span-creation):

- `kind`:
[SpanKind](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#spankind)
defines the relationship between the Span, its parents, and its children in a
Trace. Possible values: `internal`, `server`, `client`, `producer` and
`consumer`. Defaults to `internal` if not specified.
- `attributes`: See
[Attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/common/common.md#attributes)
for details about Attributes. Default is an empty list of attributes.
- `links`: List of [Links](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/overview.md#links-between-spans) to causally related Spans from the same or a different Trace.
- `start_time`: The start time of the Span operation. Defaults to the current
time. The option should only be set if the start of the operation described by
the Span has already passed.

current_span_ctx(ctx)

set_current_span(span_ctx)

When using `start_span` instead of `with_span` there must be a corresponding
call to the [end Span
API](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#end)
to signal that the operation described by the Span has ended. `end_span`
optionally takes a timestamp to use as the end time of the Span.

``` erlang
?end_span()
?end_span(Timestamp)
```

``` elixir
OpenTelemetry.Tracer.end_span(timestamp \\ :undefined)
```

#### Setting Attributes

[Setting
Attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#set-attributes)
can be done with a single key and value passed to `set_attribute` or through a
map of
[Attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/common/common.md#attributes)
all at once. Setting an attribute with a key that already exists in the Span's
map of attributes will result in that key's value being overwritten.

``` erlang
?set_attribute(Key, Value)
?set_attributes(Attributes)
```

``` elixir
OpenTelemetry.Tracer.set_attribute(key, value)
OpenTelemetry.Tracer.set_attributes(attributes)
```

Be aware that there are [configurable limits](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/common/common.md#attribute-limits) on the number and size of
Attributes per Span.

#### Adding Events

[Adding
Events](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#add-events)
can be done by passing the name of the event and the
[Attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/common/common.md#attributes)
to associate with it or as a list of Events. Each Event in the list of Events is
a map containing the timestamp, name, and Attributes which can be created with
the function `event/2` and `event/3` in the `opentelemetry` and `OpenTelemetry`
modules.

``` erlang
?add_event(Name, Attributes)
?add_events(Events)
```

``` elixir
OpenTelemetry.Tracer.add_event(event, attributes)
OpenTelemetry.Tracer.add_events(events)
```

#### Setting the Status

[Set
Status](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#set-status)
will override the default Span Status of `Unset`. A Status is a code (`ok`,
`error` or `unset`) and, only if the code is `error`, an optional message string
that describes the error.

``` erlang
?set_status(Code, Message)
```

``` elixir
OpenTelemetry.Tracer.set_status(code, message)
```

#### Update Span Name

[Updating the Span
name](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/trace/api.md#updatename)
can be done after starting the Span but must be done before the Span is end'ed.

``` erlang
?update_name(Name)
```

``` elixir
OpenTelemetry.Tracer.update_name(name)
```

### Including the OpenTelemetry SDK

When only the API is available at runtime a no-op Tracer is used and no Traces
Expand All @@ -54,7 +217,7 @@ not as a dependency of any individual Application.

Included in the same [Github
repo](https://github.com/open-telemetry/opentelemetry-erlang) as the API and SDK are an exporter for the [OpenTelemetry Protocol
(OTLP)](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md)
(OTLP)](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/protocol/otlp.md)
and [Zipkin](https://zipkin.io/):

- [OpenTelemetry Protocol](https://hex.pm/packages/opentelemetry_exporter)
Expand Down
7 changes: 5 additions & 2 deletions apps/opentelemetry_api/include/otel_tracer.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@
-define(add_events(Events),
otel_span:add_events(?current_span_ctx, Events)).

-define(set_status(Status),
otel_span:set_status(?current_span_ctx, Status)).
-define(set_status(Code, Message),
otel_span:set_status(?current_span_ctx, Code, Message)).

-define(set_status(StatusOrCode),
otel_span:set_status(?current_span_ctx, StatusOrCode)).

-define(update_name(Name),
otel_span:update_name(?current_span_ctx, Name)).
8 changes: 8 additions & 0 deletions apps/opentelemetry_api/lib/open_telemetry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ defmodule OpenTelemetry do
"""
@type status() :: :opentelemetry.status()

@type status_code() :: :opentelemetry.status_code()

defdelegate get_tracer(name), to: :opentelemetry
defdelegate get_tracer(name, vsn, schema_url), to: :opentelemetry
defdelegate set_default_tracer(t), to: :opentelemetry
Expand Down Expand Up @@ -188,6 +190,12 @@ defmodule OpenTelemetry do
@spec events(list()) :: [event()]
defdelegate events(event_list), to: :opentelemetry

@doc """
Creates a Status with an empty description.
"""
@spec status(:opentelemetry.status_code()) :: status()
defdelegate status(code), to: :opentelemetry

@doc """
Creates a Status.
"""
Expand Down
10 changes: 10 additions & 0 deletions apps/opentelemetry_api/lib/open_telemetry/tracer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ defmodule OpenTelemetry.Tracer do
:otel_span.add_events(:otel_tracer.current_span_ctx(), events)
end

@doc """
Creates and sets the Status of the currently active Span.
If used, this will override the default Span Status, which is `:unset`.
"""
@spec set_status(OpenTelemetry.status_code(), String.t()) :: boolean()
def set_status(code, message) do
:otel_span.set_status(:otel_tracer.current_span_ctx(), code, message)
end

@doc """
Sets the Status of the currently active Span.
Expand Down
Loading

0 comments on commit 2574d64

Please sign in to comment.