Simple OpenTelemetry logger in Rust

Kosta Malsev
4 min readAug 19, 2024

--

This article demonstrates how to integrate OpenTelemetry, a vendor-neutral observability framework, into a Rust application.

It specifically focuses on exporting logs using the gRPC OpenTelemetry exporter and visualizing them in Jaeger, a popular distributed tracing tool using it as both : OpenTelemetry collector and trace viewer.

High level design

We will perform in 5 simple steps:

  1. Install the prerequisites: rust and docker support.
  2. Clone the repo (link), assuming git already installed.
  3. Build the rust demo application with OpenTelemetry exporter.
  4. Build the docker image.
  5. Run the docker image and visualize the result.

Step 1: Install the prerequisites: rust and docker

Resources: rust and docker support.

Step 2: Clone the repo

git clone https://github.com/KostaMalsev/rust-logger.git

Step 3: Build the rust demo application with open-telemetry exporter

Run in main directory:

cargo build

Cargo.toml contains the following crates:

[package]
name = "simpletel"
version = "0.0.1"
edition = "2021"
description = "simple rust app demo for opentel"
license = "MIT OR Apache-2.0"

[dependencies]
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version="0.12.0", features = ["tonic", "metrics"] }
opentelemetry-semantic-conventions = { version="0.11.0" }
tokio = { version = "1.0", features = ["full"] }
rand = "0.8.5"

Adding the tokio runtime, tonic feature for the OpenTelemetry crates.

main.rs contains the following:

use opentelemetry::{
global, runtime,
sdk::{propagation::TraceContextPropagator, trace, Resource},
trace::{TraceContextExt, TraceError, Tracer},
Key, KeyValue,
};
use opentelemetry_otlp::WithExportConfig;
use rand::Rng;

// create a constant key
const RANDOM: Key = Key::from_static_str("random.value");

fn init_tracer() -> Result<trace::Tracer, TraceError> {
// Initialise OTLP Pipeline
opentelemetry_otlp::new_pipeline()
.tracing() // create OTLP tracing pipeline
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic() // create GRPC layer
.with_endpoint("http://host.docker.internal:4317"), // GRPC OTLP Jaeger Endpoint
)
// Trace provider configuration
.with_trace_config(
trace::config().with_resource(Resource::new(vec![KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
"rust-otlp-basic",
)])),
)
.install_batch(runtime::Tokio) // configure a span exporter
}

fn gen_number() -> u32 {
let mut rng = rand::thread_rng();
rng.gen()
}


#[tokio::main]
async fn main() -> Result<(), TraceError> {

// intialise the tracer
let tracer = init_tracer().unwrap();

// start a new active span
tracer.in_span("generating number", |cx| {
let span = cx.span();
let num = gen_number();
span.add_event(
"opentel demo event Generating Number".to_string(),
vec![Key::new("number").i64(num.into())],
);

// set an active span attribute
span.set_attribute(RANDOM.i64(10));


// start a new span
tracer.in_span("generate another number", |cx| {
let span = cx.span();
let num = gen_number();
span.add_event(
"Generating Number".to_string(),
vec![Key::new("number").i64(num.into())],
)
})
});

// gracefully shutdown the tracer
global::shutdown_tracer_provider();
Ok(())
}

> In main.rs initialize the tracer with init_tracer().

> Using tonic() as a rust gRPC implementation. It creates a gRPC layer to export our tracers (for tracing pipelines).

> The endpoint to the gRPC OTLP endpoint for Jaeger set to http://host.docker.internal:4317, in order to use it inside the docker container.

> Note: for test it with “cargo run” change it to http://localhost:4317.

> The init_tracer() creates a tracer object, with which we open a new span with in_span method. Adding an event with attributes.

> For more detailed line by line code explanation can try free tier Claude with “explain this code…”.

Step 4: Build the docker with following command:

docker build simpletel .

The Docker file contains the following:

# syntax=docker/dockerfile:1

FROM --platform=$BUILDPLATFORM rust:1.70 as build
ARG TARGETARCH

RUN echo "export PATH=/usr/local/cargo/bin:$PATH" >> /etc/profile
WORKDIR /app

COPY ["./platform.sh", "./"]
RUN ./platform.sh
COPY ["./config", ".cargo/config"]
RUN rustup target add $(cat /.platform)
RUN apt-get update && apt-get install -y $(cat /.compiler)

COPY ["./Cargo.toml", "./Cargo.lock", "./"]

RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release --target=$(cat /.platform)

COPY ["./src", "./src"]

RUN touch src/main.rs && cargo build --release --target=$(cat /.platform)

RUN mkdir -p /release/$TARGETARCH
RUN cp ./target/$(cat /.platform)/release/simpletel /release/$TARGETARCH/simpletel

FROM gcr.io/distroless/cc-debian11
ARG TARGETARCH

COPY --from=build /release/$TARGETARCH/simpletel /usr/local/bin/simpletel

# Set the command to run the simpletel binary
CMD ["/usr/local/bin/simpletel"]

Built on a minimalistic Debian image, the Dockerfile adds the Rust compiler to compile the app. The platform.sh script automatically configures the build environment for cross-compilation based on the target architecture.

Step 5: Run the demo application with Jaeger

docker compose up

docker-compose.yml contains the following:

version: '3.8'

services:
simpletel:
image: simpletel:latest
environment:
- RUST_LOG=debug
volumes:
- ./src:/app/src
depends_on:
- jaeger

jaeger:
image: jaegertracing/all-in-one:latest
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "6831:6831/udp" # UDP port for Jaeger agent
- "16686:16686" # Web UI
- "14268:14268" # HTTP port for spans
- "4317:4317" # gRPC port for spans

volumes:
rust-app-data:

There are two services: simpletel service runs the rust demo app which sends the traces to Jaeger service which is listening on port 4317 for gRPC OpenTelemetry traffic.

The “simpletel:latest” docker image which contains our Rust demo app with OTLP exporter is created in the Step 4.

Visualize trace in Jaeger at http://localhost:16686

Yaeger with the trace from the demo app
Jaeger output with the trace from the rust demo app at http://localhost:16686

Design considerations:

Jaeger was chosen as the OpenTelemetry collector and trace viewer for this example due to its simplicity. However, Splunk is another viable option.

Another option to test the OpenTelemetry exporter is to run in docker a simple collector in this repo.(reference)

It’s also feasible to run a standalone OpenTelemetry collector for multiple trace viewers to access (link).

The gRPC (tonic): was used due to its compact binary format, which widely used.

Summary:

This article shows how to create a Rust app that sends logs/traces using OpenTelemetry exporter to an OpenTelemetry collector-viewer (Jaeger) running in docker container. The logs/traces are displayed on Jaeger via browser.

Resources:

--

--

Kosta Malsev
Kosta Malsev

Written by Kosta Malsev

Lead SWE, ex-Meta. I love to build and share.

No responses yet