How to begin with Traces in Crystal

Michael Nikitochkin
The Incident Commander
3 min readJun 15, 2022

TL;DR: Primitve two code examples to start working with Traces

Kirk Haines³ recently started working on making opentelemetry available for Crystal applications¹. I collected the primitive examples that help understand distributed traces.

First spans to stdout

OpenTelemetry supports a different kinds of exporters (in order to visualize and analyze your traces and metrics, you will need to export them to a backend). The basic and default one is to print traces directly to STDOUT. I find it very useful — exporter prints all details of collected information.

# stdout_exporter_sample.cr
require "opentelemetry-api"
STDOUT.sync = trueOpenTelemetry.configure do |config|
config.service_name = "stdout_exporter_sample"
config.service_version = "0.1.0"
config.sampler = OpenTelemetry::Sampler::AlwaysOn.new
config.exporter = OpenTelemetry::Exporter.new(variant: :stdout)
end
tracer = OpenTelemetry.tracer_provider.tracer
tracer.in_span("first_operation") do |root_span|
root_span.consumer!
root_span.set_attribute("foo", "BAR")
root_span.set_attribute("url", "http://example.com/foo")
root_span.add_event("dispatching logs") tracer.in_span("inner_operation") do |child_span|
child_span.add_event("handling request")
tracer.in_span("get_user_from_db") do |child_span|
child_span.producer!
child_span.add_event("querying database")
end
end
tracer.in_span("process_second_stage") do |child_span|
child_span.add_event("checked permissions")
tracer.in_span("write_to_storage") do |child_span|
child_span.producer!
child_span.add_event("insert user")
end
end
end
# Make sure all exporter stdout finish in exporter fiber
sleep(1)

Execute the script and check output

$ crystal run stdout_exporter_sample.cr
{
“type”:”trace”,
“traceId”:”1479beb8000eda2fcff1fe91c777feb6",

First spans via OTLP

Of course, STDOUT is good only in local or test environments. The common protocol for OpenTelemetry is OTLP. For testing purposes I use Jaeger², but you can pick any other solutions: local or remote services.
Jaeger² supports the OTLP protocol (besides its own solution). It simplifies our local environment to run trace collector and UI in a single process.
Run Jaeger in the background with docker:

$ docker run -d — name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:1.35

You can access UI with http://localhost:16686 .
Modify the previous example a bit:

# otlp_http_exporter_sample.cr
require "opentelemetry-api"
# Another way to configure OTLP exporter in case different hostname
ENV["OTEL_EXPORTER_OTLP_ENDPOINT"] ||= "http://localhost:4318/v1/traces"
OpenTelemetry.configure do |config|
config.service_name = "otlp_http_sample"
config.service_version = "0.1.1"
config.sampler = OpenTelemetry::Sampler::AlwaysOn.new
config.exporter = OpenTelemetry::Exporter.new(variant: "http") do |c|
# NOTICE: It allows to flush spans faster and not to wait for 100 spans or 5 seconds
cc = c.as(OpenTelemetry::Exporter::Http)
cc.batch_threshold = 5
cc.batch_latency = 1
end

end
tracer = OpenTelemetry.tracer_provider.tracer
puts "Check trace: http://localhost:16686/trace/#{tracer.trace_id.hexstring}"
tracer.in_span("first_operation") do |root_span|
root_span.consumer!
root_span.set_attribute("foo", "BAR")
root_span.set_attribute("url", "http://example.com/foo")
root_span.add_event("dispatching logs") tracer.in_span("inner_operation") do |child_span|
child_span.add_event("handling request")
tracer.in_span("get_user_from_db") do |child_span|
child_span.producer!
child_span.add_event("querying database")
end
end
tracer.in_span("process_second_stage") do |child_span|
child_span.add_event("checked permissions")
tracer.in_span("write_to_storage") do |child_span|
child_span.producer!
child_span.add_event("insert user")
end
end
end
sleep(2)

Open Jaeger’s Web UI on http://localhost:16686/ and check traces. The single trace view would looks like:

Jeager: Trace view
That’s all Folks
That’s all Folks

Michael Nikitochkin is a Lead Software Engineer. Follow him on LinkedIn or GitHub.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories.

--

--

Michael Nikitochkin
The Incident Commander

software engineer. like ruby, crystal and golang. play with containers and kubernetes via terraform.