opentelemetry-traceable 0.2.0

Per-function, multi-instrumentation, dynamically-configurable tracing library on top of opentelemetry-rust
Documentation
# opentelemetry-traceable

Fast, per-function, multi-instrumentation tracing on top of [`opentelemetry-rust`](https://github.com/open-telemetry/opentelemetry-rust).
Allows generating multiple, independent traces where each span can be enabled/disabled at runtime.

## Quick start

Annotate the functions you may want to trace:

```rust
#[traceable]
fn process() { /* ... */ }

#[traceable(name = "checkout")]
async fn checkout_order() { /* ... */ }
```

Now tracing can be enabled individually for the `process` and `checkout_order` functions, at runtime.

Tracing is controlled using an **`Instrumentation`**.

Each `Instrumentation` binds:

1. A `Tracer`, which controls how the Instrumentation's traces are exported
2. An **enabled set** of functions, which controls the shape of this Instrumentation's traces

`Instrumentations` are isolated: it's possible to produce multiple/different traces off of the same function set by enabling different subsets of functions in different Instrumentations.

```rust
let instr = Instrumentation::builder()
    .tracer(provider.tracer("checkout-debug"))
    .build()?;

// the `process` function uses the default key: `my-crate::my-module::process`
// while `checkout_order` was renamed to `checkout` so it can be enabled
// without using a pattern:
instr.set_enabled(&["*::process", "checkout"])?;
```

### Test it out

```
cargo run --example basic -p opentelemetry-traceable
```

Check out the [`examples`](https://github.com/samugi/opentelemetry-traceable/tree/main/opentelemetry-traceable/examples) directory.

## Performance

Every `#[traceable]` function is disabled for every instrumentation by default. When no instrumentation is tracing a function, the annotation costs a single atomic load. The overhead is dominated by span creation, so for a given function, it is proportional to the number of instrumentations that `enable` that function.

```
Benchmark                      Mean (ns)
------------------------------ ---------
no_macro                          854.05
one_disabled                      856.19
one_enabled                      1209.67
tracing_instrument_disabled       963.46
tracing_instrument_enabled       1883.93
```

Benchmarked with Criterion. `no_macro` is the uninstrumented baseline, `one_enabled`/`one_disabled` show #[traceable] overhead with a single instrumentation toggled on/off, compared against tracing's #[instrument] macro under similar conditions.

## In-process and distributed instrumentations

An instrumentation is **in-process** by default: `in-process` `Instrumentations` are isolated, their spans can only be child of another span within the same `in-process` `Instrumentation`. Many `in-process` `Instrumentations` can coexist.

A `distributed` `Instrumentation` can be obtained by adding `.distributed()` to the builder.

```rust
let edge = Instrumentation::builder()
    .name("edge")
    .tracer(provider.tracer("edge"))
    .distributed()
    .build()?;
```

Unlike `in-process`, the `distributed` `Instrumentation` is unique, global, it interacts with OpenTelemetry's `current` Context, so it can join a propagated Context (e.g. via `traceparent`). Because distributed instrumentations are global, only one can exist at a time.

## Selecting functions by key

A selector is either an exact registry key or a pattern. `*` matches any set of characters.

```text
my_app::domain::db::*   matches  my_app::domain::db::query
                        matches  my_app::domain::db::users::insert
                        does NOT match  my_app::domain::db
*::db::*                matches  any db function, in any crate or module
*                       matches  everything
```

## Development

Tooling is managed with [`mise`](https://mise.jdx.dev/) (config at `.config/mise/config.toml`):

```
mise run test
mise run lint
```