Skip to content

Repository files navigation

PROJECT STATUS

This is a fork of the original otel-cli project, actively maintained by Amy & the bots 🤖

Built originally by A. Tobey while at Equinix Metal, this fork continues development with a focus on being a great command-line tool for OpenTelemetry signals. Starting with v0.6.0, development uses agent-assisted workflows to maintain code quality while preserving the project's philosophy.

The project is 100% Apache 2 licensed code. Development is tracked via GitHub issues and pull requests.

Get in touch:

otel-cli

otel-cli is a command-line tool for sending OpenTelemetry traces. It is written in Go and intended to be used in shell scripts and other places where the best option available for sending spans is executing another program.

otel-cli can be added to your scripts with no configuration and it will run as normal but in non-recording mode and will emit no traces. This follows the OpenTelemetry community's philosophy of "first, do no harm" and makes it so you can add otel-cli to your code and later turn it on.

Since otel-cli needs to connect to the OTLP endpoint on each run, it is highly recommended to use a localhost opentelemetry collector that can buffer spans so that the connection cost does not slow down your program too much.

Getting Started

We publish a number of package formats for otel-cli, including tar.gz, zip (windows), apk (Alpine), rpm (Red Hat variants), deb (Debian variants), and a brew tap. These can be found on the repo's Releases page.

On most platforms the easiest way is go install:

go install github.com/tobert/otel-cli@latest

Docker images are published for each otel-cli release as well:

docker pull ghcr.io/tobert/otel-cli:latest
docker run ghcr.io/tobert/otel-cli:latest status

To use the brew tap on macOS or Linux:

brew install tobert/otel-cli/otel-cli

Verify a download

Every release is born signed in public CI: the signing identity is the release workflow at that tag, witnessed by the Sigstore transparency log — no maintainer key to steal or trust. Two independent checks; either one is sufficient.

With the gh CLI, SLSA build provenance is one command against any file (or the image) from the release:

gh attestation verify otel-cli_0.6.0_linux_amd64.tar.gz -R tobert/otel-cli
gh attestation verify oci://ghcr.io/tobert/otel-cli:0.6.0 -R tobert/otel-cli

With cosign ≥ 2.5 (no GitHub tooling needed), verify the signed checksum manifest once and it covers every file it lists. Grab checksums.txt and checksums.txt.sigstore.json from the release, substituting the tag you downloaded in the identity:

cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity "https://github.com/tobert/otel-cli/.github/workflows/release.yml@refs/tags/vX.Y.Z" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  checksums.txt
sha256sum -c --ignore-missing checksums.txt

Each release page carries the same commands with the tag filled in.

Alternatively, clone the repo and build it locally:

git clone git@github.com:tobert/otel-cli.git
cd otel-cli
go build

Examples

# run otel-cli as a local OTLP server and print traces to your console
# run this in its own terminal and try some of the commands below!
otel-cli server tui

# configure otel-cli to talk the the local server spawned above
export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317

# run a program inside a span
otel-cli exec --service my-service --name "curl google" curl https://google.com

# otel-cli propagates context via envvars so you can chain it to create child spans
otel-cli exec --kind producer "otel-cli exec --kind consumer sleep 1"

# if a traceparent envvar is set it will be automatically picked up and
# used by span and exec. use --tp-ignore-env to ignore it even when present
export TRACEPARENT=00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

# you can pass the traceparent to a child via arguments as well
# {{traceparent}} in any of the command's arguments will be replaced with the traceparent string
otel-cli exec --name "curl api" -- \
   curl -H 'traceparent: {{traceparent}}' https://myapi.com/v1/coolstuff

# create a span with a custom start/end time using either RFC3339,
# same with the nanosecond extension, or Unix epoch, with/without nanos
otel-cli span --start 2021-03-24T07:28:05.12345Z --end 2021-03-24T07:30:08.0001Z
otel-cli span --start 1616620946 --end 1616620950.241980634
# so you can do this:
start=$(date --rfc-3339=ns) # rfc3339 with nanoseconds
some-interesting-program --with-some-options
end=$(date +%s.%N) # Unix epoch with nanoseconds
otel-cli span -n my-script -s some-interesting-program --start $start --end $end

# for advanced cases you can start a span in the background, and
# add events to it, finally closing it later in your script
sockdir=$(mktemp -d)
otel-cli span background \
   --service $0          \
   --name "$0 runtime"   \
   --sockdir $sockdir & # the & is important here, background server will block
sleep 0.1 # give the background server just a few ms to start up
otel-cli span event --name "cool thing" --attrs "foo=bar" --sockdir $sockdir
otel-cli span end --sockdir $sockdir
# or you can kill the background process and it will end the span cleanly
kill %1

# server mode can also write traces to the filesystem, e.g. for testing
dir=$(mktemp -d)
otel-cli server json --dir $dir --timeout 60 --max-spans 5

Configuration

Everything is configurable via CLI arguments, json config, and environment variables. If no endpoint is specified, otel-cli will run in non-recording mode and not attempt to contact any servers.

All three modes of config can be mixed. Command line args are loaded first, then config file, then environment variables.

CLI argument environment variable config file key example value
--endpoint OTEL_EXPORTER_OTLP_ENDPOINT endpoint localhost:4317
--traces-endpoint OTEL_EXPORTER_OTLP_TRACES_ENDPOINT traces_endpoint https://localhost:4318/v1/traces
--protocol OTEL_EXPORTER_OTLP_PROTOCOL protocol http/protobuf
--insecure OTEL_EXPORTER_OTLP_INSECURE insecure false
--timeout OTEL_EXPORTER_OTLP_TIMEOUT timeout 1s
--otlp-headers OTEL_EXPORTER_OTLP_HEADERS otlp_headers k=v,a=b
--otlp-blocking OTEL_EXPORTER_OTLP_BLOCKING otlp_blocking false
--config OTEL_CLI_CONFIG_FILE config_file config.json
--verbose OTEL_CLI_VERBOSE verbose false
--fail OTEL_CLI_FAIL fail false
--service OTEL_SERVICE_NAME service_name myapp
--kind OTEL_CLI_TRACE_KIND span_kind server
--status-code OTEL_CLI_STATUS_CODE span_status_code error
--status-description OTEL_CLI_STATUS_DESCRIPTION span_status_description cancelled
--attrs OTEL_CLI_ATTRIBUTES span_attributes k=v,a=b
--force-trace-id OTEL_CLI_FORCE_TRACE_ID force_trace_id 00112233445566778899aabbccddeeff
--force-span-id OTEL_CLI_FORCE_SPAN_ID force_span_id beefcafefacedead
--force-parent-span-id OTEL_CLI_FORCE_PARENT_SPAN_ID force_parent_span_id eeeeeeb33fc4f3d3
--tp-required OTEL_CLI_TRACEPARENT_REQUIRED traceparent_required false
--tp-carrier OTEL_CLI_CARRIER_FILE traceparent_carrier_file filename.txt
--tp-ignore-env OTEL_CLI_IGNORE_ENV traceparent_ignore_env false
--tp-print OTEL_CLI_PRINT_TRACEPARENT traceparent_print false
--tp-export OTEL_CLI_EXPORT_TRACEPARENT traceparent_print_export false
--tls-no-verify OTEL_CLI_TLS_NO_VERIFY tls_no_verify false
--tls-ca-cert OTEL_EXPORTER_OTLP_CERTIFICATE tls_ca_cert /ca/ca.pem
--tls-client-key OTEL_EXPORTER_OTLP_CLIENT_KEY tls_client_key /keys/client-key.pem
--tls-client-cert OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE tls_client_cert /keys/client-cert.pem

Valid timeout units are "ns", "us"/"µs", "ms", "s", "m", "h".

Endpoint URIs

otel-cli deviates from the OTel specification for endpoint URIs. Mainly, otel-cli supports bare host:port for grpc endpoints and continues to default to gRPC. The optional http/json is not supported by opentelemetry-go so otel-cli does not support it. To use gRPC with an http endpoint, set the protocol with --protocol or the envvar.

  • bare host:port endpoints are assumed to be gRPC and are not supported for HTTP
  • http:// and https:// are assumed to be HTTP unless --protocol is set to grpc.
  • loopback addresses without an https:// prefix are assumed to be unencrypted

Header and Attribute formatting

Headers and attributes allow for key=value,k=v style formatting. Internally both otel-cli and pflag use Go's encoding/csv to parse these values. Therefore, if you want to pass commas in a value, follow CSV quoting rules and quote the whole k=v pair. Double quotes need to be escaped so the shell doesn't interpolate them. Once that's done, embedding commas will work fine.

otel-cli span --attrs item1=value1,\"item2=value2,value3\",item3=value4
otel-cli span --attrs 'item1=value1,"item2=value2,value3",item3=value4'

Docker TLS Certificates

Since release 0.4.2, otel-cli containers are built off an Alpine base image (pinned by digest in release/Dockerfile and bumped by Dependabot) which contains the base CA certificate bundles. In order to override these for e.g. a self-signed certificate, the best bet is to volume mount your own /etc/ssl into the container, and it should get picked up by otel-cli and Go's TLS libraries.

docker run -v /etc/ssl:/etc/ssl ghcr.io/tobert/otel-cli:latest status

Easy local dev

We want working on otel-cli to be easy, so we've provided a few different ways to get started. In general, there are three things you need:

  • A working Go environment
  • A built (or installed) copy of otel-cli itself
  • A system to receive/inspect the traces you generate

1. A working Go environment

Providing instructions on getting Go up and running on your machine is out of scope for this README. However, the good news is that it's fairly easy to do! You can follow the normal Installation instructions from the Go project itself.

2. A built (or installed) copy of otel-cli itself

If you're planning on making changes to otel-cli, we recommend building the project locally: go build

But, if you just want to quickly try out otel-cli, you can also just install it directly: go get github.com/tobert/otel-cli. This will place the command in your GOPATH. If your GOPATH is in your PATH you should be all set.

3. A system to receive/inspect the traces you generate

otel-cli can run as a server and accept OTLP connections. It has two modes, one prints to your console while the other writes to JSON files.

otel-cli server tui
otel-cli server json --dir $dir --timeout 60 --max-spans 5

Many SaaS vendors accept OTLP these days so one option is to send directly to those. This is not recommended for production since it will slow your code down on the roundtrips. It is recommended to use an opentelemetry-collector locally.

Another option is to use otel-desktop-viewer. This will bring up a server that can accept OTLP connections.

If you're not sure what to choose, try otel-cli server tui or otel-desktop-viewer.

otel-desktop-viewer setup

# install the CLI tool
go install github.com/CtrlSpice/otel-desktop-viewer@latest

# run it!
$(go env GOPATH)/bin/otel-desktop-viewer

# if you have $GOPATH/bin added to your $PATH you can call it directly!
otel-desktop-viewer

# if not you can add it to your $PATH by running this or adding it to
# your startup script (usually ~/.bashrc or ~/.zshrc)
export PATH="$(go env GOPATH)/bin:$PATH"

The OpenTelemetry collector is listening on localhost:4318, and the UI will be running on localhost:8000.

# start the desktop viewer (best to do this in a separate terminal)
otel-desktop-viewer

# configure otel-cli to send to our desktop viewer endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

# use otel-cli to generate spans!
otel-cli exec --service my-service --name "curl google" curl https://google.com

This trace will be available at localhost:8000.

SaaS tracing vendor

We've provided Honeycomb, LightStep, and Elastic configurations that you could also use, if you're using one of those vendors today. It's still pretty easy to get started:

# optional: to send data to an an OTLP-enabled tracing vendor, pass in your
# API auth token over an environment variable and modify
# `local/otel-vendor-config.yaml` according to the comments inside
export LIGHTSTEP_TOKEN= # Lightstep API key (otlp/1 in the yaml)
export HONEYCOMB_TEAM=  # Honeycomb API key (otlp/2 in the yaml)
export ELASTIC_TOKEN= # Elastic token for the APM server.

docker run \
   --env LIGHTSTEP_TOKEN \
   --env HONEYCOMB_TEAM \
   --env ELASTIC_TOKEN \
   --name otel-collector \
   --net host \
   --volume $(pwd)/configs/otel-vendor-config.yaml:/local.yaml \
   otel/opentelemetry-collector-contrib:0.101.0 \
      --config /local.yaml

Then it should just work to run otel-cli:

./otel-cli span -n "testing" -s "my first test span"
# or for quick iterations:
go run . span -n "testing" -s "my first test span"

Contributing

We welcome contributions! This project uses agent-assisted development starting with v0.6.0.

For agents working on this codebase:

  • See BOTS.md for comprehensive agent development guidance
  • CLAUDE.md and GEMINI.md symlink to BOTS.md for model-specific access
  • Emphasize functional testing in main_test.go
  • Commit freely in branches, squash on merge to main

For human contributors:

  • File issues for bugs or feature requests
  • Submit PRs with clear descriptions
  • See TESTING.md for testing guidance
  • Follow the existing Go code style

Releases

Releases are cut by GitHub Actions: pushing a v* tag runs release.yml, which drives goreleaser (.goreleaser.yml) to build every platform, publish the GitHub release with archives, apk/deb/rpm packages, checksums and an SPDX SBOM, push the Homebrew formula to tobert/homebrew-otel-cli, and then builds, signs, attests and tags the multiarch ghcr.io/tobert/otel-cli image. Everything is signed keylessly with cosign and carries SLSA build provenance; see Verify a download.

To cut a release:

git checkout main && git pull --rebase   # release tags come off main
# update CHANGELOG.md for the new version and merge that first
git tag v0.6.0                            # a -rc.N suffix makes a GitHub prerelease and skips the tap
git push origin v0.6.0                    # the tag push triggers release.yml
gh run watch                              # optional: follow along

To smoke the whole matrix without publishing, run the workflow by hand from the Actions tab (workflow_dispatch), or locally:

goreleaser release --snapshot --clean --skip=publish,sign,sbom

The workflow needs one repository secret, HOMEBREW_TAP_GITHUB_TOKEN: a fine-grained personal access token with Contents: read and write on the tap repository only. GITHUB_TOKEN covers the release and the container registry. The workflow refuses to start a tagged release without it rather than fail after the GitHub release is already published.

License

Apache License 2.0 - Copyright (c) 2025 A. Tobey

See LICENSE for full details.

About

OpenTelemetry command-line tool for sending events from shell scripts & similar environments

Resources

Stars

13 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages