# otel-cli: sending OpenTelemetry spans from a shell script

> otel-cli is an Apache 2.0 licensed Go tool for sending OpenTelemetry traces from shell scripts and other places where the only option is to run another program, with a local server mode, trace context propagation by environment variable and configuration through flags, a JSON file and the standard OTEL environment variables. The judgement is that the most important design decision is that it does nothing unless you tell it where to send, and that the configuration precedence is the detail to check first.

**equinix-labs/otel-cli** — OpenTelemetry command-line tool for sending events from shell scripts & similar environments

- Repository: https://github.com/equinix-labs/otel-cli
- Stars: 723 · Forks: 59
- Language: Go
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/equinix-labs-otel-cli

## A span sender for the places where a library is not an option

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

That is a narrower problem than it sounds and a more common one. A service written in Go or Python can hold an SDK and emit spans directly. A deployment script, a database migration, a cron job that copies files, a makefile target, none of those have a language runtime you can instrument, and all of them want to show up in the same trace as the service that triggered them. Wrapping the command is the only option there.

The repository reflects that narrowness. There is an otelcli directory for the command line layer, an otlpclient directory for the client side of the protocol and an otlpserver directory for the local server, a w3c directory for the trace context handling, and a main.go at the root. The dependency list is short and obvious, the OpenTelemetry SDK and the OTLP protocol definitions, gRPC, protobuf, cobra for the command line parsing and pterm for the terminal output.

The badge at the top of the readme says it plainly, the stability level is experimental, and the licence is Apache 2.0. The last push to the repository was on 2026-10-20, while the newest release listed is v0.4.5 from 2024-04-01, so the tree is active and the published artefacts are old.

## It does nothing until you tell it where to send

The design decision that makes the tool safe to add to a script is in the first paragraph after the introduction.

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. The readme connects that to the OpenTelemetry community's philosophy of first, do no harm, and draws the conclusion that you can add otel-cli to your code and turn it on later.

The mechanism is simple and stated again in the configuration section. If no endpoint is specified, otel-cli will run in non-recording mode and will not attempt to contact any servers.

For a tool that exists to be sprinkled through shell scripts, this is the difference between a dependency you can adopt in an afternoon and one you have to schedule. The failure mode of a misconfigured tracer is a script that hangs waiting for a collector that is not there, and non-recording mode removes that failure mode entirely. The cost is that a script can be running for weeks with the traces going nowhere, so the switch to a real endpoint is a deliberate step.

There is one more piece of operational advice, and it is about performance rather than correctness. Since otel-cli needs to connect to the OTLP endpoint on each run, the readme recommends using a localhost OpenTelemetry collector that can buffer spans, so that the connection cost does not slow the program down too much.

## Three commands cover the useful cases

The command set is small, and the readme's walkthrough is the best documentation of it.

The server command runs otel-cli as a local OTLP server and prints the traces to your console, which the readme suggests you run in its own terminal while you try the other commands. Pointing the tool at that server is a single environment variable, OTEL_EXPORTER_OTLP_ENDPOINT set to localhost:4317.

The exec command runs a program inside a span:

```shell
otel-cli exec --service my-service --name "curl google" curl https://google.com
```

The span command creates a span without wrapping anything, which is what you want when the interesting interval is between two steps rather than around one, and it accepts explicit start and end times in RFC 3339, in RFC 3339 with the nanosecond extension, or in Unix epoch with or without nanoseconds. That matters for scripts, because the timestamps can be captured with date before and after the work:

```shell
start=$(date --rfc-3339=ns)
some-interesting-program --with-some-options
end=$(date +%s.%N)
otel-cli span -n my-script -s some-interesting-program --start $start --end $end
```

The server mode can also write traces to the filesystem, which is what the json subcommand with a directory, a timeout and a maximum span count is for, and that is the mode to use in a test.

## Context travels by environment variable or by placeholder

Chaining a script to a service, or a script to another script, is the part that makes traces useful across process boundaries, and otel-cli handles it two ways.

The first is automatic. If a traceparent environment variable is set, it is picked up and used by the span and exec commands, and there is a flag to ignore it even when it is present. The second is the placeholder. A traceparent written as {{traceparent}} in any of the command's arguments is replaced with the traceparent string, so a curl invocation can carry the header explicitly:

```shell
otel-cli exec --name "curl api" -- \
   curl -H 'traceparent: {{traceparent}}' https://myapi.com/v1/coolstuff
```

The placeholder form exists because not every program can be taught to read an environment variable, and a raw header in the command line is the lowest common denominator. The readme also shows the producer and consumer pattern, where one otel-cli exec runs another otel-cli exec, and the inner one inherits the context and creates the child span.

The background mode is the third piece and it is the most unusual. A span can be started in the background with a socket directory, events can be added to it while it runs, and it can be closed later, and killing the background process ends the span cleanly, which is a graceful degradation for a script that exits early. The readme notes that the ampersand at the end of the start line matters, because the background server blocks.

## Three configuration sources, and the one that wins

The configuration section is a long table, and the rule at the top of it is the part to remember.

Everything is configurable through command line arguments, a JSON configuration file and environment variables. All three modes of config can be mixed. The load order is stated explicitly: command line args are loaded first, then the config file, then environment variables.

So the environment wins, and that is the opposite of the convention most tools follow, where a flag beats a file which beats an environment variable. The reason is defensible, since the standard OTEL environment variables are what a container platform or a CI system sets, and they are usually the last thing you want to override, but it is also the thing that will surprise you. A pipeline that exports OTEL_EXPORTER_OTLP_ENDPOINT will silently beat the endpoint in your configuration file.

The table itself is worth scanning, because it is a list of the OpenTelemetry conventions the tool honours. The endpoint and traces endpoint map to the standard OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TRACES_ENDPOINT variables, the protocol, the insecure flag, the timeout, the extra headers and the blocking mode all follow the same naming, and the service name maps to OTEL_SERVICE_NAME.

The trace context settings have their own group, with a required flag, a carrier file, an ignore-environment flag and a print flag, and there are forced identifiers for the trace, the span and the parent span, which is what you use when you are writing a test and need the identifiers to be stable.

## Every package format, a scratch image and a local collector

The distribution section covers a lot of ground, because the audience is people running the tool in containers, on macOS laptops and on Linux servers.

The project publishes tar.gz archives, a zip for Windows, an apk for Alpine, an rpm for the Red Hat variants, a deb for the Debian variants and a Homebrew tap, all on the releases page. The simplest route on most platforms is go install github.com/equinix-labs/otel-cli@latest. On macOS the tap is equinix-labs/otel-cli followed by brew install otel-cli. Docker images are published for each release under ghcr.io/equinix-labs/otel-cli, and the readme shows running one with the status subcommand. Building from a clone is a go build.

The container image is a good example of the small footprint the tool is aiming at. The builder stage uses the Go image with CGO disabled and strips the binary with the standard linker flags, and the final stage is scratch with a single copied binary. There is no shell and no package manager in the running image, which is what you want in something that gets invoked from inside another script.

The compose file in the repository is the local development environment, and it is the collector arrangement the readme recommends. It runs Jaeger in the all-in-one image on the usual Jaeger ports and an OpenTelemetry Collector on port 4317, with the collector's configuration mounted from the configs directory, and the collector depends on Jaeger.

The packaging is automated with a goreleaser configuration, and dependencies are kept current with a renovate configuration, and there is a TESTING.md file and a changelog. The w3c directory and the tls test file are small details that say the trace context and the TLS paths are tested rather than assumed.

## Conclusion

otel-cli fits a team with shell scripts, cron jobs and deployment steps that need to appear in a trace, because the tool wraps a program in a span without changing the program and without asking for configuration up front. It is the wrong choice if you need a stable production dependency, since the readme carries an experimental stability badge and the newest published release is from April 2024 even though the repository is being worked on, and it is the wrong choice if you want structured logs or metrics, since the tool is about traces. Before you adopt it, put a collector on localhost so the connection cost does not land inside your script, and check the configuration precedence, because the environment variables are loaded last and therefore override both the command line flags and the configuration file.

## FAQ

### What is otel-cli?

It is an Apache 2.0 licensed Go command line tool for sending OpenTelemetry traces, intended for shell scripts and other places where the only option is to run another program. The readme marks its stability level as experimental.

### What happens if I do not configure an endpoint?

otel-cli runs as normal but in non-recording mode, emitting no traces and not attempting to contact any server. That is deliberate, following the OpenTelemetry principle of first, do no harm, so you can add it to scripts before you turn it on.

### How does otel-cli propagate trace context?

It picks up a traceparent environment variable automatically for the span and exec commands, and it replaces a {{traceparent}} placeholder in any argument with the traceparent string, so a child command can carry the header explicitly. The ignore-environment flag turns off the automatic pickup.

### Which configuration source wins?

Command line arguments are loaded first, then the JSON config file, then the environment variables, so the environment has the final say. The table in the readme maps each flag to its standard OpenTelemetry environment variable, such as OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME.

## Sources

- [equinix-labs/otel-cli on GitHub](https://github.com/equinix-labs/otel-cli)
- [Issues](https://github.com/equinix-labs/otel-cli/issues)
- [License: Apache-2.0](https://github.com/equinix-labs/otel-cli/blob/main/LICENSE)
- [README](https://github.com/equinix-labs/otel-cli/blob/main/README.md)
- [Releases](https://github.com/equinix-labs/otel-cli/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/equinix-labs-otel-cli
