Showing preview only (317K chars total). Download the full file or copy to clipboard to get everything.
Repository: equinix-labs/otel-cli
Branch: main
Commit: 8f86e487b5f5
Files: 62
Total size: 299.3 KB
Directory structure:
gitextract_c7r88qzy/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .goreleaser.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── TESTING.md
├── configs/
│ ├── otel-collector.yaml
│ └── otel-vendor-config.yaml
├── data_for_test.go
├── demos/
│ ├── 01-simple-span.sh
│ ├── 05-nested-exec.sh
│ ├── 10-span-background-simple.sh
│ ├── 15span-background-layered.sh
│ ├── 20span-background-race-workarounds.sh
│ ├── 25srecon22-talk-agenda.sh
│ └── 30-trace-build-process/
│ └── otel-wrapper-shim.sh
├── docker-compose.yaml
├── example-config.json
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── otelcli/
│ ├── completion.go
│ ├── config.go
│ ├── config_span.go
│ ├── config_span_test.go
│ ├── config_test.go
│ ├── config_tls.go
│ ├── diagnostics.go
│ ├── exec.go
│ ├── otlpclient.go
│ ├── root.go
│ ├── server.go
│ ├── server_json.go
│ ├── server_tui.go
│ ├── span.go
│ ├── span_background.go
│ ├── span_background_server.go
│ ├── span_end.go
│ ├── span_event.go
│ ├── status.go
│ ├── version.go
│ └── version_test.go
├── otlpclient/
│ ├── otlp_client.go
│ ├── otlp_client_grpc.go
│ ├── otlp_client_grpc_test.go
│ ├── otlp_client_http.go
│ ├── otlp_client_http_test.go
│ ├── otlp_client_null.go
│ ├── otlp_client_test.go
│ ├── protobuf_span.go
│ └── protobuf_span_test.go
├── otlpserver/
│ ├── grpcserver.go
│ ├── httpserver.go
│ └── server.go
├── release/
│ └── Dockerfile
├── renovate.json
├── tls_for_test.go
└── w3c/
└── traceparent/
├── traceparent.go
└── traceparent_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [ main ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- name: Setup
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Get dependencies
run: go mod download
# otel-cli's main test needs the binary built ahead of time
# also this validates it can acutally build before we get there
- name: Build
# build with -s -w to reduce binary size and verify that build in test
run: go build -v -ldflags="-s -w -X main.version=test -X main.commit=${{ github.sha }}"
- name: Test
run: go test -v -cover -parallel 4 ./...
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
otel-cli
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
dist/
*.pem
================================================
FILE: .goreleaser.yml
================================================
# last updated for goreleaser v1.14.1
before:
hooks:
- go mod tidy
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: 'SNAPSHOT-{{ .Commit }}'
changelog:
sort: asc
filters:
exclude:
- '^demos:'
- '^configs:'
- Merge pull request
- Merge branch
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- arm64
- 386
goarm:
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm64
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}
nfpms:
- package_name: otel-cli
homepage: https://github.com/equinix-labs/otel-cli
maintainer: Amy Tobey <atobey@equinix.com>
description: OpenTelemetry CLI Application (Server & Client)
license: Apache 2.0
formats:
- apk
- deb
- rpm
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip
builds_info:
group: root
owner: root
rlcp: true
brews:
# This means the repository must be equinix-labs/homebrew-otel-cli
- name: "otel-cli"
url_template: "https://github.com/equinix-labs/otel-cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
tap:
owner: "equinix-labs"
name: "homebrew-otel-cli"
token: "{{ .Env.GITHUB_TOKEN }}"
commit_author:
name: "tobert"
email: "atobey@equinix.com"
homepage: "https://github.com/equinix-labs/otel-cli"
description: "OpenTelemetry command-line tool for sending events from shell scripts & similar environments"
license: "Apache-2.0"
# If set to auto, the release will not be uploaded to the homebrew tap
# in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1
skip_upload: "auto"
dockers:
- image_templates:
- "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-amd64"
dockerfile: release/Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
- image_templates:
- "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-arm64v8"
dockerfile: release/Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm64/v8"
docker_manifests:
- name_template: "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}"
image_templates:
- "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-amd64"
- "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-arm64v8"
- name_template: "ghcr.io/equinix-labs/otel-cli:latest"
image_templates:
- "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-amd64"
- "ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-arm64v8"
use: docker
================================================
FILE: CHANGELOG.md
================================================
## [0.4.6] - 2024-05-13
Build smaller binaries and add version subcommand.
### Added
- `otel-cli version` will now report version information of the binary (if any)
### Changed
- linker flags now contain `-s -w` to reduce binary size
- change made in build Dockerfile, goreleaser, and GH Actions
- contribution from @Ipmi-13, thank you!
- goreleaser now does the -X flags so `otel-cli version` will work right
- removed Content-Length from functional tests bc it's not fixed with gzip in play
- updated demos, added one
- updated instructions in README.md
## [0.4.5] - 2024-01-01
Fix exec attributes, cleanups, and dependency updates.
`otel-cli exec` attributes were broken for the last few releases so @tobert
felt it was ok to rename them to match the OTel semantic conventions
now.
### Changed
- using latest deps for grpc, protobuf, and otel
- exec attributes will now go out with the span
- exec now sends process.command and process.command_args attributes
- main_test.go now supports regular expression tests on span data
## [0.4.4] - 2024-03-11
Fix a typo in the `OTEL_CLI_EXEC_TP_DISABLE_INJECT` envvar.
### Changed
- spell `DISALBE` correctly in `OTEL_CLI_EXEC_TP_DISABLE_INJECT`
- adds a test for that
## [0.4.3] - 2024-03-11
Add injection of `{{traceparent}}` to `otel-cli exec` as default behavior, along with
the `otel-cli exec --tp-disable-inject` to turn it off (old behavior).
### Added
- `otel-cli exec echo {{traceparent}}` is now supported to pass traceparent to child process
- `otel-cli exec --tp-disable-inject` will disable this new default behavior
## [0.4.2] - 2023-12-01
The Docker container now builds off `alpine:latest` instead of `scratch`. This
makes the default certificate store included with Alpine available to otel-cli.
### Changed
- switch release Dockerfile to base off alpine:latest
## [0.4.1] - 2023-10-16
Mostly small but impactful changes to `otel-cli exec`.
### Added
- `otel-cli exec --command-timeout 30s` provides a separate command timeout from the otel timeout
- SIGINT is now caught and passed to the child process
- attributes can be set or overwrite on a backgrounded span via `otel-cli span end`
### Changed
- bumped several dependencies to the latest release
- updated README.md
## [0.4.0] - 2023-08-09
This focus of this release is a brand-new OTLP client implementation. It has fewer features
than the opentelemetry-collector code, and allows for more fine-grained control over how
gRPC and HTTP are configured. Along the way, the `otelcli` and `otlpclient` packages went
through a couple refactors to organize code better in preparation for adding metrics and
logs, hopefully in 0.5.0.
### Added
- `--force-parent-span-id` allows forcing the span parent (thanks @domofactor!)
- `otel-cli status` now includes a list of errors including retries that later succeeded
### Changed
- `--otlp-blocking` is marked deprecated and no longer does anything
- the OTLP client implementation is no longer using opentelemetry-collector
- traceparent code is now in a w3c/traceparent package
- otlpserver.CliEvent is removed entirely, preferring protobuf spans & events
## [0.3.0] - 2023-05-26
The most important change is that `otel-cli exec` now treats arguments as an argv
list instead of munging them into a string. This shouldn't break anyone doing sensible
things, but could if folks were expecting the old `sh -c` behavior.
Envvars are no longer deleted before calling exec. Actually, they still are, but otel-cli
backs up its envvars early so they can be propagated anyways.
The rest of the visible changes are incremental additions or fixes. As for the invisible,
otel-cli now generates span protobufs directly, and no longer goes through the
opentelemetry-go SDK. This ended up making some code cleaner, aside from some of the
protobuf-isms that show up.
A user has a use case for setting custom trace and span ids, so the `--force-trace-id`
and `--force-span-id` options were added to `span`, `exec`, and other span-generating
subcommands.
### Added
- `--force-trace-id` and `--force-span-id`
- `--status-code` and `--status-description` are supported, including on `otel-cli span background end`
- demo for working around race conditions when using `span background`
- more functional tests, including some regression tests
- functional test harness now has a way to do custom checks via CheckFuncs
- added this changelog
### Changed
- (behavior) reverted envvar deletion so envvars will propagate through `exec` again
- (behavior) exec argument handling is now precise and no longer uses `sh -c`
- build now requires Go 1.20 or greater
- otel-cli now generates span protobufs directly instead of using opentelemetry-go
- respects signal-specific configs per OTel spec
- handle endpoint configs closer to spec
- lots of little cleanups across the code and docs
- many dependency updates
## [0.2.0] 2023-02-27
The main addition in this version is client mTLS authentication support, which comes in with
extensive e2e tests for TLS settings.
`--no-tls-verify` is deprecated in favor of `--tls-no-verify` so all the TLS options are consistent.
`otel-cli span background` now has a `--background-skip-parent-pid-check` option for some use cases
where folks want otel-cli to keep running past its parent process.
### Changed
- 52f1143 #11 support OTEL_SERVICE_NAME per spec (#158)
- ed4bf2f Bump golang.org/x/net from 0.5.0 to 0.7.0 (#159)
- 5c5865c Make configurable skipping pid check in span background command (#161)
- 7214b64 Replace Jaeger instructions with otel-desktop-viewer (#162)
- 6018f76 TLS naming cleanup (#166)
- 9a7de86 add TLS testing and client certificate auth (#150)
- 759fbef miscellaneous documentation fixes (#165)
- f5286c0 never allow insecure automatically for https:// URIs (#149)
## [0.1.0] 2023-02-02
Apologies for the very long delay between releases. There is a lot of pent-up change
in this release.
Bumped minor version to 0.1 because there are some changes in behavior around
endpoint URI handling and configuration. Also some inconsistencies in command line
arguments has been touched up, so some uses of single-letter flags and `--ignore-tp-env`
(renamed to `--tp-ignore-env to match other flags) might break.
Viper has been dropped in favor of directly loading configuration from json and
environment variables. It appears none of the Viper features ever worked in
otel-cli so it shouldn't be a big deal, but if you were using Viper configs they
won't work anymore and you'll have to switch to otel-cli's json config format.
Endpoints now conform mostly to the OTel spec, except for a couple cases
documented in the README.md.
### Changed
- 4256644 #108 fix span background attrs (#116)
- e8b86f6 #142 follow spec for OTLP protocol (#148)
- efb5608 #42 add version subcommand (#114)
- 007f8f7 Add renovate.json (#123)
- 9d7a668 Make the service/name CLI short args match the long args (#110)
- b164427 add http testing (#143)
- 72df644 docs: --ignore-tp-env replace field to --tp-ignore-env (#147)
- e48e468 feat: add span status code/description cli and env var (#111)
- ce850f4 make grpc server stop more robust (#122)
- 8eb37fb remove viper, fix tests, fix and expand envvars (#120)
- ff5a4eb update OTel to 1.4.1 (#107)
- b51d6fc update goreleaser config, add release procedure in README.md (#141)
- 99c9242 update opentelemetry SDK to 1.11.2 (#138)
## [0.0.x] - 2021-03-24 - 2022-02-24
Developing the base functionality. Light on testing.
### Changed
- added OTLP test server
- added goreleaser
- added timeouts
- many refactors while discovering the shape of the tool
- switch to failing silently
- added subcommand to generate autocomplete data
- added status subcommand
- added functional test harness
- added HTTP support
================================================
FILE: Dockerfile
================================================
FROM golang:latest AS builder
WORKDIR /build
COPY . .
ENV CGO_ENABLED=0
RUN go build -ldflags="-w -s" -o otel-cli .
FROM scratch AS otel-cli
COPY --from=builder /build/otel-cli /otel-cli
ENTRYPOINT ["/otel-cli"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Packet Host, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# otel-cli
[](https://github.com/packethost/standards/blob/master/experimental-statement.md)
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](https://github.com/equinix-labs/otel-cli/releases) page.
On most platforms the easiest way is a go get:
```shell
go install github.com/equinix-labs/otel-cli@latest
```
Docker images are published for each otel-cli release as well:
```shell
docker pull ghcr.io/equinix-labs/otel-cli:latest
docker run ghcr.io/equinix-labs/otel-cli:latest status
```
To use the brew tap e.g. on MacOS:
```shell
brew tap equinix-labs/otel-cli
brew install otel-cli
```
Alternatively, clone the repo and build it locally:
```shell
git clone git@github.com:equinix-labs/otel-cli.git
cd otel-cli
go build
```
## Examples
```shell
# 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](https://pkg.go.dev/time#ParseDuration) 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.
```shell
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
As of release 0.4.2, otel-cli containers are built off the latest Alpine base
image 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.
```shell
docker run -v /etc/ssl:/etc/ssl ghcr.io/equinix-labs/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](https://golang.org/doc/install) 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/equinix-labs/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.
```shell
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`](https://github.com/CtrlSpice/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
```shell
# 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`.
```shell
# 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:
```shell
# 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:
```shell
./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
Please file issues and PRs on the GitHub project at https://github.com/equinix-labs/otel-cli
## Releases
Releases are managed by goreleaser. Currently this is limited to @tobert due to rules in
the equinix-labs organization. For now releases are not automated, but will be by the time
a v1.0 rolls out and the test suite is robust enough that we feel confident.
Testing the release: `goreleaser release --snapshot --rm-dist`
To release, a GitHub personal access token is required. The release also needs to be tagged
in git.
```shell
docker login ghcr.io # log into GitHub Docker repo
gh repo list # make sure GitHub PAT is working
git checkout main # release tags must be off the main branch
git pull --rebase # get the latest HEAD
git tag v0.1.1 # tag HEAD with the next version
git push --tags # push new tag up to GitHub
goreleaser release --rm-dist
```
## License
Apache 2.0, see LICENSE
================================================
FILE: TESTING.md
================================================
# Testing otel-cli
## Synopsis
otel-cli's primary method of testing is functional, implemented in
`main_test.go` and accompanying files. It sets up a server and runs an otel-cli
build through a number of tests to verify that everything from environment
variable passing to server TLS negotiation work as expected.
## Unit Testing
Do it. It doesn't have to be fancy, just exercise the code a little. It's more
about all of us being able to iterate quickly than reaching total coverage.
Most unit tests are in the `otelcli` package. The tests in the root of this
project are not unit tests.
## The otel-cli Test Harness
When `go test` is run in the root of this project, it runs the available
`./otel-cli` binary through a suite of tests, providing otel-cli with its
endpoint information (via templates) and examining the payloads received on the
server.
The otel-cli test harness is more complex than otel-cli itself. Its goal is to
be able to test that setting e.g. `OTEL_EXPORTER_OTLP_CLIENT_KEY` works all the
way through to authenticating to a TLS server. The bugs are going to exist in
the glue code, since that's mostly what otel-cli is. Each of Cobra,
`encoding/json`, and opentelemetry-go are thorougly unit and battle tested. So,
otel-cli tests a build in a functional test harness.
Tests are defined in `data_for_test.go` in Go data structures. Suites are a
group of Fixtures that go together. Mostly Suites are necessary for the
backgrounding feature, to test e.g. `otel-cli span background`, and to organize
tests by functionality, etc.. Fixtures configure everything for an otel-cli
invocation, and what to expect back from it.
The OTLP server functionality originally built for `otel-cli server tui` is
re-used in the tests to run a server in a goroutine. It supports both gRPC and
HTTP variants of OTLP, and can be configured with TLS. This allows otel-cli to
connect to a server and send traces, which the harness then compares to
expectations defined in the test Fixture.
otel-cli has a special subcommand, `otel-cli status` that sends a span and
reports back on otel-cli's internal state. The test harness detects status
commands and can check data in it.
`tls_for_test.go` implements an ephemeral certificate authority that is created
and destroyed on each run. The rest of the test harness injects the CA and certs
created into the tests, allowing for full-system testing.
A Fixture can be configured to run "in the background". In this case, the harness
will behave as if you ran the command `./otel-cli &` and let following fixtures
run on top of it. This is mostly used to test `otel-cli span background`, which
exists primarily to run as a backgrounded job in a shell script. When background
jobs are in use, be careful with test names as they are used as a key to manage
the process.
## Adding Tests
For a new area of functionality, you'll want to add a Suite. A Suite is mostly
for organization of tests, but is also used to manage state when testing background
jobs. A Fixture is made of two parts: an otel-cli command configuration, and a
data structure of expected results. The harness presents otel-cli with the exact
ARGV specified in `Config.CliArgs`, and a clean environment with only the envvars
provided in the `Env` stringmap. The output from otel-cli is captured with stdout
and stderr combined. This can be tested against as well.
It is often wise to pass `"--fail", "--verbose"` to CliArgs for debugging and it's
fine to leave them on permanently. Without them otel-cli will be silent about
failures and you'll get a confusing test output.
Most of the time it's best to copy an existing Suite or Fixture and adjust it to
the case you're trying to test. Please try to clean out any unneeded config when
you do this so the tests are easy to understand. It's not bad to to test a little
extra surface area, just try to keep things readable.
================================================
FILE: configs/otel-collector.yaml
================================================
---
receivers:
otlp:
protocols:
grpc:
exporters:
logging:
loglevel: debug
jaeger:
endpoint: "jaeger:14250"
tls:
insecure: true
processors:
batch:
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger,logging]
================================================
FILE: configs/otel-vendor-config.yaml
================================================
# opentelemetry-collector is a proxy for telemetry events.
#
# This configuration is set up for use in otel-cli development.
# With collector in debug mode every trace is printed to the console
# which makes working on otel-cli quick & easy. There are also
# examples below for how to send to Lightstep and Honeycomb.
receivers:
otlp:
protocols:
# OTLP over gRPC
grpc:
endpoint: "0.0.0.0:4317"
# OTLP over HTTP (opentelemetry-ruby only works on this proto for now)
http:
endpoint: "0.0.0.0:55681"
processors:
batch:
exporters:
# set to detailed and your traces will get printed to the console spammily
debug:
verbosity: detailed
# Lightstep: set & export LIGHTSTEP_TOKEN and enable below
otlp/1:
endpoint: "ingest.lightstep.com:443"
headers:
"lightstep-access-token": "${LIGHTSTEP_TOKEN}"
# Honeycomb: set & export HONEYCOMB_TEAM to the auth token
# You may also want to set HONEYCOMB_DATASET for legacy accounts.
otlp/2:
endpoint: "api.honeycomb.io:443"
headers:
"x-honeycomb-team": "${HONEYCOMB_TEAM}"
# Elastic: set & export the ELASTIC_TOKEN to the auth token for the APM server.
otlp/3:
endpoint: "xxx.elastic-cloud.com:443"
headers:
Authorization: "Bearer ${ELASTIC_TOKEN}"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
# only enable debug by default
exporters: [debug]
# Lightstep:
# exporters: [debug, otlp/1]
# Honeycomb:
# exporters: [debug, otlp/2]
# Elastic:
# exporters: [debug, otlp/3]
================================================
FILE: data_for_test.go
================================================
package main_test
// This file implements data structures and data for functional testing of
// otel-cli.
//
// See: TESTING.md
//
// TODO: Results.SpanData could become a struct now
import (
"os"
"regexp"
"syscall"
"testing"
"time"
"github.com/equinix-labs/otel-cli/otelcli"
"github.com/equinix-labs/otel-cli/otlpclient"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
)
type serverProtocol int
const (
grpcProtocol serverProtocol = iota
httpProtocol
)
// CheckFunc is a function that gets called after the test is run to do
// custom checking of values.
type CheckFunc func(t *testing.T, fixture Fixture, results Results)
type FixtureConfig struct {
CliArgs []string
Env map[string]string
// timeout for how long to wait for the whole test in failure cases
TestTimeoutMs int
// when true this test will be excluded under go -test.short mode
// TODO: maybe move this up to the suite?
IsLongTest bool
// either grpcProtocol or httpProtocol, defaults to grpc
ServerProtocol serverProtocol
// sets up the server with the test CA, requiring TLS
ServerTLSEnabled bool
// tells the server to require client certificate authentication
ServerTLSAuthEnabled bool
// for timeout tests we need to start the server to generate the endpoint
// but do not want it to answer when otel-cli calls, this does that
StopServerBeforeExec bool
// run this fixture in the background, starting its server and otel-cli
// instance, then let those block in the background and continue running
// serial tests until it's "foreground" by a second fixtue with the same
// description in the same file
Background bool
Foreground bool
// for testing signal behavior a time to wait before sending a signal
// and the signal to send can be specified
KillAfter time.Duration
KillSignal os.Signal
}
// mostly mirrors otelcli.StatusOutput but we need more
type Results struct {
// same as otelcli.StatusOutput but copied because embedding doesn't work for this
Config otelcli.Config `json:"config"`
SpanData map[string]string `json:"span_data"`
Env map[string]string `json:"env"`
Diagnostics otelcli.Diagnostics `json:"diagnostics"`
Errors otlpclient.ErrorList `json:"errors"`
// these are specific to tests...
ServerMeta map[string]string
Headers map[string]string // headers sent by the client
ResourceSpans *tracepb.ResourceSpans
CliOutput string // merged stdout and stderr
CliOutputRe *regexp.Regexp // regular expression to clean the output before comparison
SpanCount int // number of spans received
EventCount int // number of events received
TimedOut bool // true when test timed out
CommandFailed bool // otel-cli was killed, did not exit() on its own
ExitCode int // the process exit code returned by otel-cli
Span *tracepb.Span
SpanEvents []*tracepb.Span_Event
}
// Fixture represents a test fixture for otel-cli.
type Fixture struct {
Name string
Config FixtureConfig
Endpoint string
TlsData TlsSettings
Expect Results
CheckFuncs []CheckFunc
}
// FixtureSuite is a list of Fixtures that run serially.
type FixtureSuite []Fixture
var suites = []FixtureSuite{
// otel-cli should not do anything when it is not explicitly configured"
{
{
Name: "nothing configured",
Config: FixtureConfig{
CliArgs: []string{"status"},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
Diagnostics: otelcli.Diagnostics{
IsRecording: false,
NumArgs: 1,
ParsedTimeoutMs: 1000,
},
},
},
},
// setting minimum envvars should result in a span being received
{
{
Name: "minimum configuration (recording, grpc)",
Config: FixtureConfig{
ServerProtocol: grpcProtocol,
CliArgs: []string{"status", "--endpoint", "{{endpoint}}"},
TestTimeoutMs: 1000,
},
Expect: Results{
// otel-cli should NOT set insecure when it auto-detects localhost
Config: otelcli.DefaultConfig().
WithEndpoint("{{endpoint}}").
WithInsecure(false),
ServerMeta: map[string]string{
"proto": "grpc",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
}, {
Name: "minimum configuration (recording, http)",
Config: FixtureConfig{
ServerProtocol: httpProtocol,
CliArgs: []string{"status", "--endpoint", "http://{{endpoint}}"},
TestTimeoutMs: 1000,
},
Expect: Results{
// otel-cli should NOT set insecure when it auto-detects localhost
Config: otelcli.DefaultConfig().
WithEndpoint("http://{{endpoint}}").
WithInsecure(false),
ServerMeta: map[string]string{
"content-type": "application/x-protobuf",
"host": "{{endpoint}}",
"method": "POST",
"proto": "HTTP/1.1",
"uri": "/v1/traces",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
},
// everything works as expected with fully-loaded options
{
{
Name: "fully loaded command line",
Config: FixtureConfig{
ServerProtocol: grpcProtocol,
TestTimeoutMs: 1000,
Env: map[string]string{
"TRACEPARENT": "00-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-AAAAAAAAAAAAAAAA-01",
},
CliArgs: []string{
"span",
"--endpoint", "{{endpoint}}",
"--protocol", "grpc",
"--insecure",
"--timeout", "500000us", // 500ms
"--fail", "--verbose",
"--name", "slartibartfast",
"--service", "Starship Bistromath",
"--kind", "server",
"--start", "308534400",
"--end", "1979-10-12T23:59:59Z",
"--attrs", "protagonist=DentArthurdent,medium=book",
"--otlp-headers", "lue=42",
"--force-span-id", "BBBBBBBBBBBBBBBB",
"--tp-required",
"--tp-print",
"--tp-export",
},
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig(),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 8,
ParsedTimeoutMs: 1000,
DetectedLocalhost: true,
InsecureSkipVerify: true,
},
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": `medium=book,protagonist=DentArthurdent`,
},
Headers: map[string]string{
":authority": "{{endpoint}}\n",
"content-type": "application/grpc\n",
"user-agent": "*",
"lue": "42\n",
},
CliOutput: "" +
"# trace id: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" +
"# span id: bbbbbbbbbbbbbbbb\n" +
"export TRACEPARENT=00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01\n",
},
},
},
// TLS connections
{
{
Name: "minimum configuration (tls, no-verify, recording, grpc)",
Config: FixtureConfig{
ServerProtocol: grpcProtocol,
CliArgs: []string{
"status",
"--endpoint", "https://{{endpoint}}",
"--protocol", "grpc",
// TODO: switch to --tls-no-verify before 1.0, for now keep testing it
"--verbose", "--fail", "--no-tls-verify",
},
TestTimeoutMs: 1000,
ServerTLSEnabled: true,
},
Expect: Results{
Config: otelcli.DefaultConfig().
WithEndpoint("https://{{endpoint}}").
WithProtocol("grpc").
WithVerbose(true).
WithTlsNoVerify(true),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 8,
DetectedLocalhost: true,
InsecureSkipVerify: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "minimum configuration (tls, no-verify, recording, https)",
Config: FixtureConfig{
ServerProtocol: httpProtocol,
CliArgs: []string{"status", "--endpoint", "https://{{endpoint}}", "--tls-no-verify"},
TestTimeoutMs: 2000,
ServerTLSEnabled: true,
},
Expect: Results{
// otel-cli should NOT set insecure when it auto-detects localhost
Config: otelcli.DefaultConfig().
WithTlsNoVerify(true).
WithEndpoint("https://{{endpoint}}"),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 4,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "minimum configuration (tls, client cert auth, recording, grpc)",
Config: FixtureConfig{
ServerProtocol: grpcProtocol,
CliArgs: []string{
"status",
"--endpoint", "https://{{endpoint}}",
"--protocol", "grpc",
"--verbose", "--fail",
"--tls-ca-cert", "{{tls_ca_cert}}",
"--tls-client-cert", "{{tls_client_cert}}",
"--tls-client-key", "{{tls_client_key}}",
},
TestTimeoutMs: 1000,
ServerTLSEnabled: true,
ServerTLSAuthEnabled: true,
},
Expect: Results{
Config: otelcli.DefaultConfig().
WithEndpoint("https://{{endpoint}}").
WithProtocol("grpc").
WithTlsCACert("{{tls_ca_cert}}").
WithTlsClientKey("{{tls_client_key}}").
WithTlsClientCert("{{tls_client_cert}}").
WithVerbose(true),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 13,
DetectedLocalhost: true,
InsecureSkipVerify: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "minimum configuration (tls, client cert auth, recording, https)",
Config: FixtureConfig{
ServerProtocol: httpProtocol,
CliArgs: []string{
"status",
"--endpoint", "https://{{endpoint}}",
"--verbose", "--fail",
"--tls-ca-cert", "{{tls_ca_cert}}",
"--tls-client-cert", "{{tls_client_cert}}",
"--tls-client-key", "{{tls_client_key}}",
},
TestTimeoutMs: 2000,
ServerTLSEnabled: true,
ServerTLSAuthEnabled: true,
},
Expect: Results{
Config: otelcli.DefaultConfig().
WithEndpoint("https://{{endpoint}}").
WithTlsCACert("{{tls_ca_cert}}").
WithTlsClientKey("{{tls_client_key}}").
WithTlsClientCert("{{tls_client_cert}}").
WithVerbose(true),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 11,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
},
// ensure things fail when they're supposed to fail
{
// otel is configured but there is no server listening so it should time out silently
{
Name: "timeout with no server",
Config: FixtureConfig{
CliArgs: []string{"span", "--timeout", "1s"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
},
// this needs to be less than the timeout in CliArgs
TestTimeoutMs: 500,
IsLongTest: true, // can be skipped with `go test -short`
StopServerBeforeExec: true, // there will be no server listening
},
Expect: Results{
Config: otelcli.DefaultConfig(),
// we want and expect a timeout and failure
TimedOut: true,
CommandFailed: true,
},
},
{
Name: "syntax errors in environment variables cause the command to fail",
Config: FixtureConfig{
CliArgs: []string{"span", "--fail", "--verbose"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_CLI_VERBOSE": "lmao", // invalid input
},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
ExitCode: 1,
// strips the date off the log line before comparing to expectation
CliOutputRe: regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `),
CliOutput: "Error while loading environment variables: could not parse OTEL_CLI_VERBOSE value " +
"\"lmao\" as an bool: strconv.ParseBool: parsing \"lmao\": invalid syntax\n",
},
},
{
Name: "https:// should fail when TLS is not available",
Config: FixtureConfig{
ServerProtocol: httpProtocol,
CliArgs: []string{"status", "--endpoint", "https://{{endpoint}}"},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig().
WithEndpoint("https://{{endpoint}}"),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 0,
},
CheckFuncs: []CheckFunc{
func(t *testing.T, f Fixture, r Results) {
want := injectVars(`Post "https://{{endpoint}}/v1/traces": http: server gave HTTP response to HTTPS client`, f.Endpoint, f.TlsData)
if len(r.Errors) >= 1 {
if r.Errors[0].Error != want {
t.Errorf("Got the wrong error: %q", r.Errors[0].Error)
}
} else {
t.Errorf("Expected at least one error but got %d.", len(r.Errors))
}
},
},
},
},
// regression tests
{
{
// The span end time was missing when #175 merged, which showed up
// as 0ms spans. CheckFuncs was added to make this possible. This
// test runs sleep for 10ms and checks the duration of the span is
// at least 10ms.
Name: "#189 otel-cli exec sets span start time earlier than end time",
Config: FixtureConfig{
// note: relies on system sleep command supporting floats
// note: 10ms is hardcoded in a few places for this test and commentary
CliArgs: []string{"exec", "--endpoint", "{{endpoint}}", "sleep", "0.01"},
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithEndpoint("grpc://{{endpoint}}"),
},
CheckFuncs: []CheckFunc{
func(t *testing.T, f Fixture, r Results) {
//elapsed := r.Span.End.Sub(r.Span.Start)
elapsed := time.Duration((r.Span.EndTimeUnixNano - r.Span.StartTimeUnixNano) * uint64(time.Nanosecond))
if elapsed.Milliseconds() < 10 {
t.Errorf("elapsed test time not long enough. Expected 10ms, got %d ms", elapsed.Milliseconds())
}
},
},
},
{
Name: "#181 OTEL_ envvars should persist through to otel-cli exec",
Config: FixtureConfig{
CliArgs: []string{"status"},
Env: map[string]string{
"OTEL_FAKE_VARIABLE": "fake value",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_EXPORTER_OTLP_CERTIFICATE": "{{tls_ca_cert}}",
"X_WHATEVER": "whatever",
},
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}").WithTlsCACert("{{tls_ca_cert}}"),
Env: map[string]string{
"OTEL_FAKE_VARIABLE": "fake value",
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_EXPORTER_OTLP_CERTIFICATE": "{{tls_ca_cert}}",
"X_WHATEVER": "whatever",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
DetectedLocalhost: true,
NumArgs: 1,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
},
},
{
Name: "#200 custom trace path in general endpoint gets signal path appended",
Config: FixtureConfig{
CliArgs: []string{"status", "--endpoint", "http://{{endpoint}}/mycollector"},
ServerProtocol: httpProtocol,
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithEndpoint("http://{{endpoint}}/mycollector"),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
DetectedLocalhost: true,
NumArgs: 3,
ParsedTimeoutMs: 1000,
// spec says /v1/traces should get appended to any general endpoint URL
Endpoint: "http://{{endpoint}}/mycollector/v1/traces",
EndpointSource: "general",
},
},
},
{
Name: "#200 custom trace path on signal endpoint does not get modified",
Config: FixtureConfig{
CliArgs: []string{"status", "--traces-endpoint", "http://{{endpoint}}/mycollector/x/1"},
ServerProtocol: httpProtocol,
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithTracesEndpoint("http://{{endpoint}}/mycollector/x/1"),
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
DetectedLocalhost: true,
NumArgs: 3,
ParsedTimeoutMs: 1000,
Endpoint: "http://{{endpoint}}/mycollector/x/1",
EndpointSource: "signal",
},
},
},
{
Name: "#258 Commands that exit with a non-zero exit code should report a span",
Config: FixtureConfig{
CliArgs: []string{"exec",
"--endpoint", "{{endpoint}}",
"--verbose", "--fail",
"--", "false",
},
},
Expect: Results{
ExitCode: 1,
SpanCount: 1,
CliOutput: "",
CommandFailed: false, // otel-cli should exit voluntarily in this case
Config: otelcli.DefaultConfig().WithEndpoint("grpc://{{endpoint}}"),
},
},
{
Name: "#316 ensure process command and args are sent as attributes",
Config: FixtureConfig{
CliArgs: []string{"exec",
"--endpoint", "{{endpoint}}",
"--verbose", "--fail",
"--attrs", "zy=ab", // ensure CLI args still propagate
"--", "/bin/echo", "a", "z",
},
},
Expect: Results{
SpanCount: 1,
CliOutput: "a z\n",
SpanData: map[string]string{
"attributes": "/^process.command=/bin/echo,process.command_args=/bin/echo,a,z,process.owner=\\w+,process.parent_pid=\\d+,process.pid=\\d+,zy=ab/",
},
},
},
},
// otel-cli span with no OTLP config should do and print nothing
{
{
Name: "otel-cli span (unconfigured, non-recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "--service", "main_test.go", "--name", "test-span-123", "--kind", "server"},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// config file
{
{
Name: "load a json config file",
Config: FixtureConfig{
CliArgs: []string{"status", "--config", "example-config.json"},
// this will take priority over the config
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
},
TestTimeoutMs: 1000,
},
Expect: Results{
SpanCount: 1,
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
DetectedLocalhost: true,
Error: "could not open file '/tmp/traceparent.txt' for read: open /tmp/traceparent.txt: no such file or directory",
},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
},
Config: otelcli.DefaultConfig().
WithEndpoint("{{endpoint}}"). // tells the test framework to ignore/overwrite
WithTimeout("1s").
WithHeaders(map[string]string{"header1": "header1-value"}).
WithInsecure(true).
WithBlocking(false).
WithTlsNoVerify(true).
WithTlsCACert("/dev/null").
WithTlsClientKey("/dev/null").
WithTlsClientCert("/dev/null").
WithServiceName("configured_in_config_file").
WithSpanName("config_file_span").
WithKind("server").
WithAttributes(map[string]string{"attr1": "value1"}).
WithStatusCode("0").
WithStatusDescription("status description").
WithTraceparentCarrierFile("/tmp/traceparent.txt").
WithTraceparentIgnoreEnv(true).
WithTraceparentPrint(true).
WithTraceparentPrintExport(true).
WithTraceparentRequired(false).
WithBackgroundParentPollMs(100).
WithBackgroundSockdir("/tmp").
WithBackgroundWait(true).
WithSpanEndTime("now").
WithSpanEndTime("now").
WithEventName("config_file_event").
WithEventTime("now").
WithCfgFile("example-config.json").
WithVerbose(true).
WithFail(true),
},
},
},
// otel-cli with minimal config span sends a span that looks right
{
{
Name: "otel-cli span (recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "--service", "main_test.go", "--name", "test-span-123", "--kind", "server"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanCount: 1,
},
},
// OTEL_RESOURCE_ATTRIBUTES and OTEL_CLI_SERVICE_NAME should get merged into
// the span resource attributes
{
Name: "otel-cli span with envvar service name and attributes (recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "--name", "test-span-service-name-and-attrs", "--kind", "server"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_CLI_SERVICE_NAME": "test-service-abc123",
"OTEL_CLI_ATTRIBUTES": "cafe=deadbeef,abc=123",
"OTEL_RESOURCE_ATTRIBUTES": "foo.bar=baz",
},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": "abc=123,cafe=deadbeef",
"service_attributes": "foo.bar=baz,service.name=test-service-abc123",
},
SpanCount: 1,
},
},
// OTEL_SERVICE_NAME
{
Name: "otel-cli span with envvar service name (recording)",
Config: FixtureConfig{
CliArgs: []string{"span"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_SERVICE_NAME": "test-service-123abc",
},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"service_attributes": "service.name=test-service-123abc",
},
SpanCount: 1,
},
},
},
// otel-cli span --print-tp actually prints
{
{
Name: "otel-cli span --print-tp (non-recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "--tp-print"},
Env: map[string]string{"TRACEPARENT": "00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-01"},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
CliOutput: "" + // empty so the text below can indent and line up
"# trace id: f6c109f48195b451c4def6ab32f47b61\n" +
"# span id: a5d2a35f2483004e\n" +
"TRACEPARENT=00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-01\n",
},
},
},
// otel-cli span --print-tp propagates traceparent even when not recording
{
{
Name: "otel-cli span --tp-print --tp-export (non-recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "--tp-print", "--tp-export"},
Env: map[string]string{
"TRACEPARENT": "00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-00",
},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
CliOutput: "" +
"# trace id: f6c109f48195b451c4def6ab32f47b61\n" +
"# span id: a5d2a35f2483004e\n" +
"export TRACEPARENT=00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-00\n",
},
},
},
// otel-cli span background, non-recording, this uses the suite functionality
// and background tasks, which are a little clunky but get the job done
{
{
Name: "otel-cli span background (nonrecording)",
Config: FixtureConfig{
CliArgs: []string{"span", "background", "--timeout", "1s", "--sockdir", "."},
TestTimeoutMs: 2000,
Background: true, // sorta like & in shell
Foreground: false, // must be true later, like `fg` in shell
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span event",
Config: FixtureConfig{
CliArgs: []string{"span", "event", "--name", "an event happened", "--attrs", "ima=now,mondai=problem", "--sockdir", "."},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span end",
Config: FixtureConfig{
CliArgs: []string{"span", "end", "--sockdir", "."},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
// Name on foreground *must* match the backgrounded job
// TODO: ^^ this isn't great, find a better way
Name: "otel-cli span background (nonrecording)",
Config: FixtureConfig{
Foreground: true, // bring it back (fg) and finish up
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// otel-cli span background, in recording mode
{
{
Name: "otel-cli span background (recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "background", "--timeout", "1s", "--sockdir", ".", "--attrs", "abc=def"},
Env: map[string]string{"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}"},
TestTimeoutMs: 2000,
Background: true,
Foreground: false,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": `abc=def`, // weird format because of limitation in OTLP server
},
SpanCount: 1,
EventCount: 1,
},
// this validates options sent to otel-cli span end
CheckFuncs: []CheckFunc{
func(t *testing.T, f Fixture, r Results) {
if r.Span.Status.GetCode() != 2 {
t.Errorf("expected 2 for span status code, but got %d", r.Span.Status.GetCode())
}
if r.Span.Status.GetMessage() != "I can't do that Dave." {
t.Errorf("got wrong string for status description: %q", r.Span.Status.GetMessage())
}
},
},
},
{
Name: "otel-cli span event",
Config: FixtureConfig{
CliArgs: []string{"span", "event", "--name", "an event happened", "--attrs", "ima=now,mondai=problem", "--sockdir", "."},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span end",
Config: FixtureConfig{
CliArgs: []string{
"span", "end",
"--sockdir", ".",
// these are validated by checkfuncs defined above ^^
"--status-code", "error",
"--status-description", "I can't do that Dave.",
},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span background (recording)",
Config: FixtureConfig{
Foreground: true, // fg
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// otel-cli span background, add attrs on span end
{
{
Name: "otel-cli span background (recording) with attrs added on end",
Config: FixtureConfig{
CliArgs: []string{"span", "background", "--timeout", "1s", "--sockdir", "."},
Env: map[string]string{"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}"},
TestTimeoutMs: 2000,
Background: true,
Foreground: false,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": `abc=def,ghi=jkl`, // weird format because of limitation in OTLP server
},
SpanCount: 1,
},
},
{
Name: "otel-cli span end",
Config: FixtureConfig{
CliArgs: []string{
"span", "end",
"--sockdir", ".",
"--attrs", "ghi=jkl,abc=def",
},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span background (recording) with attrs added on end",
Config: FixtureConfig{
Foreground: true, // fg
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// otel-cli span background with attrs, append attrs on span end
{
{
Name: "otel-cli span background (recording) with attrs append on end",
Config: FixtureConfig{
CliArgs: []string{"span", "background", "--timeout", "1s", "--sockdir", ".", "--attrs", "abc=def"},
Env: map[string]string{"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}"},
TestTimeoutMs: 2000,
Background: true,
Foreground: false,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": `abc=def,ghi=jkl`, // weird format because of limitation in OTLP server
},
SpanCount: 1,
},
},
{
Name: "otel-cli span end",
Config: FixtureConfig{
CliArgs: []string{
"span", "end",
"--sockdir", ".",
"--attrs", "ghi=jkl",
},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span background (recording) with attrs append on end",
Config: FixtureConfig{
Foreground: true, // fg
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// otel-cli span background, modify and add attrs on span end
{
{
Name: "otel-cli span background (recording) with attrs modified and added on end",
Config: FixtureConfig{
CliArgs: []string{"span", "background", "--timeout", "1s", "--sockdir", ".", "--attrs", "abc=123"},
Env: map[string]string{"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}"},
TestTimeoutMs: 2000,
Background: true,
Foreground: false,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": `abc=def,ghi=jkl`, // weird format because of limitation in OTLP server
},
SpanCount: 1,
},
},
{
Name: "otel-cli span end",
Config: FixtureConfig{
CliArgs: []string{
"span", "end",
"--sockdir", ".",
"--attrs", "ghi=jkl,abc=def",
},
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
{
Name: "otel-cli span background (recording) with attrs modified and added on end",
Config: FixtureConfig{
Foreground: true, // fg
},
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// otel-cli exec runs echo
{
{
Name: "otel-cli span exec echo",
Config: FixtureConfig{
// intentionally calling a command with no args bc it's a special case in exec.go
CliArgs: []string{"exec", "--service", "main_test.go", "--name", "test-span-123", "--kind", "server", "echo"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"TRACEPARENT": "00-edededededededededededededed9000-edededededededed-01",
},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "edededededededededededededed9000",
},
CliOutput: "\n",
SpanCount: 1,
},
},
},
// otel-cli exec runs otel-cli exec
{
{
Name: "otel-cli exec (nested)",
Config: FixtureConfig{
CliArgs: []string{
"exec", "--name", "outer", "--endpoint", "{{endpoint}}", "--fail", "--verbose", "--",
"./otel-cli", "exec", "--name", "inner", "--endpoint", "{{endpoint}}", "--tp-required", "--fail", "--verbose", "echo", "hello world"},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
CliOutput: "hello world\n",
SpanCount: 2,
},
},
},
// otel-cli exec echo "{{traceparent}}" and otel-cli exec --tp-disable-inject
{
{
Name: "otel-cli exec with arg injection injects the traceparent",
Config: FixtureConfig{
CliArgs: []string{
"exec", "--endpoint", "{{endpoint}}",
"--force-trace-id", "e39280f2980af3a8600ae98c74f2dabf", "--force-span-id", "023eee2731392b4d",
"--",
"echo", "{{traceparent}}"},
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
CliOutput: "00-e39280f2980af3a8600ae98c74f2dabf-023eee2731392b4d-01\n",
SpanCount: 1,
},
},
{
Name: "otel-cli exec --tp-disable-inject returns the {{traceparent}} tag unmodified",
Config: FixtureConfig{
CliArgs: []string{
"exec", "--endpoint", "{{endpoint}}",
"--force-trace-id", "e39280f2980af3a8600ae98c74f2dabf", "--force-span-id", "023eee2731392b4d",
"--tp-disable-inject",
"--",
"echo", "{{traceparent}}"},
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
CliOutput: "{{traceparent}}\n",
SpanCount: 1,
},
},
{
Name: "otel-cli exec returns the {{traceparent}} tag unmodified with OTEL_CLI_EXEC_TP_DISABLE_INJECT",
Config: FixtureConfig{
Env: map[string]string{
"OTEL_CLI_EXEC_TP_DISABLE_INJECT": "true",
},
CliArgs: []string{
"exec", "--endpoint", "{{endpoint}}",
"--force-trace-id", "e39280f2980af3a8600ae98c74f2dabf", "--force-span-id", "023eee2731392b4d",
"--",
"echo", "{{traceparent}}"},
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
CliOutput: "{{traceparent}}\n",
SpanCount: 1,
},
},
},
// validate OTEL_EXPORTER_OTLP_PROTOCOL / --protocol
{
// --protocol
{
Name: "--protocol grpc",
Config: FixtureConfig{
ServerProtocol: grpcProtocol,
CliArgs: []string{"status", "--endpoint", "{{endpoint}}", "--protocol", "grpc"},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}").WithProtocol("grpc"),
ServerMeta: map[string]string{
"proto": "grpc",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 5,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "--protocol http/protobuf",
Config: FixtureConfig{
ServerProtocol: httpProtocol,
CliArgs: []string{"status", "--endpoint", "http://{{endpoint}}", "--protocol", "http/protobuf"},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("http://{{endpoint}}").WithProtocol("http/protobuf"),
ServerMeta: map[string]string{
"content-type": "application/x-protobuf",
"host": "{{endpoint}}",
"method": "POST",
"proto": "HTTP/1.1",
"uri": "/v1/traces",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 5,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "protocol: bad config",
Config: FixtureConfig{
CliArgs: []string{"status", "--endpoint", "{{endpoint}}", "--protocol", "xxx", "--verbose", "--fail"},
TestTimeoutMs: 1000,
},
Expect: Results{
CliOutputRe: regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `),
CliOutput: "invalid protocol setting \"xxx\"\n",
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
Diagnostics: otelcli.Diagnostics{
IsRecording: false,
NumArgs: 7,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
ExecExitCode: 1,
},
SpanCount: 0,
},
},
// OTEL_EXPORTER_OTLP_PROTOCOL
{
Name: "OTEL_EXPORTER_OTLP_PROTOCOL grpc",
Config: FixtureConfig{
ServerProtocol: grpcProtocol,
// validate protocol can be set to grpc with an http endpoint
CliArgs: []string{"status", "--endpoint", "http://{{endpoint}}"},
TestTimeoutMs: 1000,
Env: map[string]string{
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
},
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("http://{{endpoint}}").WithProtocol("grpc"),
ServerMeta: map[string]string{
"proto": "grpc",
},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf",
Config: FixtureConfig{
ServerProtocol: httpProtocol,
CliArgs: []string{"status", "--endpoint", "http://{{endpoint}}"},
TestTimeoutMs: 1000,
Env: map[string]string{
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
},
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("http://{{endpoint}}").WithProtocol("http/protobuf"),
ServerMeta: map[string]string{
"content-type": "application/x-protobuf",
"host": "{{endpoint}}",
"method": "POST",
"proto": "HTTP/1.1",
"uri": "/v1/traces",
},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
SpanCount: 1,
},
},
{
Name: "OTEL_EXPORTER_OTLP_PROTOCOL: bad config",
Config: FixtureConfig{
CliArgs: []string{"status", "--endpoint", "http://{{endpoint}}", "--fail", "--verbose"},
TestTimeoutMs: 1000,
Env: map[string]string{
"OTEL_EXPORTER_OTLP_PROTOCOL": "roflcopter",
},
},
Expect: Results{
ExitCode: 1,
CliOutputRe: regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `),
CliOutput: "invalid protocol setting \"roflcopter\"\n",
Config: otelcli.DefaultConfig().WithEndpoint("http://{{endpoint}}"),
Diagnostics: otelcli.Diagnostics{
IsRecording: false,
NumArgs: 3,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
Error: "invalid protocol setting \"roflcopter\"\n",
},
SpanCount: 0,
},
},
},
// --force-trace-id, --force-span-id and --force-parent-span-id allow setting/forcing custom trace, span and parent span ids
{
{
Name: "forced trace, span and parent span ids",
Config: FixtureConfig{
CliArgs: []string{
"status",
"--endpoint", "{{endpoint}}",
"--fail",
"--force-trace-id", "00112233445566778899aabbccddeeff",
"--force-span-id", "beefcafefacedead",
"--force-parent-span-id", "e4e3eeb33fc4f3d3",
},
},
Expect: Results{
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
SpanData: map[string]string{
"trace_id": "00112233445566778899aabbccddeeff",
"span_id": "beefcafefacedead",
"parent_span_id": "e4e3eeb33fc4f3d3",
},
SpanCount: 1,
Diagnostics: otelcli.Diagnostics{
NumArgs: 10,
IsRecording: true,
DetectedLocalhost: true,
ParsedTimeoutMs: 1000,
Endpoint: "*",
EndpointSource: "*",
},
},
},
},
// full-system test --otlp-headers makes it to grpc/http servers
{
{
Name: "#231 gRPC headers for authentication",
Config: FixtureConfig{
CliArgs: []string{
"status",
"--endpoint", "{{endpoint}}",
"--protocol", "grpc",
"--otlp-headers", "x-otel-cli-otlpserver-token=abcdefgabcdefg",
},
ServerProtocol: grpcProtocol,
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().
WithEndpoint("{{endpoint}}").
WithProtocol("grpc").
WithHeaders(map[string]string{
"x-otel-cli-otlpserver-token": "abcdefgabcdefg",
}),
Headers: map[string]string{
":authority": "{{endpoint}}\n",
"content-type": "application/grpc\n",
"user-agent": "*",
"x-otel-cli-otlpserver-token": "abcdefgabcdefg\n",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
DetectedLocalhost: true,
NumArgs: 7,
ParsedTimeoutMs: 1000,
Endpoint: "grpc://{{endpoint}}",
EndpointSource: "general",
},
},
},
{
Name: "#231 http headers for authentication",
Config: FixtureConfig{
CliArgs: []string{
"status",
"--endpoint", "http://{{endpoint}}",
"--protocol", "http/protobuf",
"--otlp-headers", "x-otel-cli-otlpserver-token=abcdefgabcdefg",
},
ServerProtocol: httpProtocol,
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().
WithEndpoint("http://{{endpoint}}").
WithProtocol("http/protobuf").
WithHeaders(map[string]string{
"x-otel-cli-otlpserver-token": "abcdefgabcdefg",
}),
Headers: map[string]string{
"Content-Type": "application/x-protobuf",
"Accept-Encoding": "gzip",
"User-Agent": "Go-http-client/1.1",
"X-Otel-Cli-Otlpserver-Token": "abcdefgabcdefg",
},
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
DetectedLocalhost: true,
NumArgs: 7,
ParsedTimeoutMs: 1000,
Endpoint: "http://{{endpoint}}/v1/traces",
EndpointSource: "general",
},
},
},
},
// exec signal and timeout behavior
{
{
Name: "handle ctrl-c gracefully",
Config: FixtureConfig{
CliArgs: []string{"exec", "--endpoint", "{{endpoint}}", "--timeout", "2s", "sleep", "1"},
KillAfter: time.Millisecond * 20,
KillSignal: syscall.SIGINT, // control-c
TestTimeoutMs: 50, // if we get to 50ms the signal failed
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
ExitCode: 2,
SpanData: map[string]string{
"status_code": "2",
"status_description": "exec command failed: signal: interrupt",
},
},
},
{
Name: "exec --command-timeout terminates processes",
Config: FixtureConfig{
CliArgs: []string{"exec",
"--endpoint", "{{endpoint}}",
"--command-timeout", "20ms",
"sleep", "1",
},
TestTimeoutMs: 50,
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
ExitCode: 2,
SpanData: map[string]string{
"status_code": "2",
"status_description": "exec command failed: signal: killed",
},
},
},
{
Name: "exec --command-timeout can run longer than --timeout",
Config: FixtureConfig{
CliArgs: []string{"exec",
"--endpoint", "{{endpoint}}",
"--command-timeout", "100ms",
"--timeout", "50ms",
"sleep", "0.75", // depends on GNU sleep's floating point sleeps
},
TestTimeoutMs: 200,
},
Expect: Results{
SpanCount: 1,
Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"),
ExitCode: 0,
},
},
},
}
================================================
FILE: demos/01-simple-span.sh
================================================
#!/bin/bash
# an otel-cli demo
# this isn't super precise because of process timing but good enough
# in many cases to be useful
st=$(date +%s.%N) # unix epoch time with nanoseconds
data1=$(uuidgen)
data2=$(uuidgen)
et=$(date +%s.%N)
# don't worry, there are also short options :)
../otel-cli span \
--service "otel-cli-demo" \
--name "hello world" \
--kind "client" \
--start $st \
--end $et \
--tp-print \
--attrs "my.data1=$data1,my.data2=$data2"
================================================
FILE: demos/05-nested-exec.sh
================================================
#!/bin/bash
# an otel-cli demo of nested exec
#
# this isn't necessarily practical, but it demonstrates how the TRACEPARENT
# environment variable carries the context from invocation to invocation
# so that the tracing provider (e.g. Honeycomb) can put it all back together
# set the service name automatically on calls to otel-cli
export OTEL_SERVICE_NAME="otel-cli-demo"
# generate a new trace & span, cli will print out the 'export TRACEPARENT'
carrier=$(mktemp)
../otel-cli span -n "traceparent demo $0" --tp-print --tp-carrier $carrier
# this will start a child span, and run another otel-cli as its program
../otel-cli exec \
--name "hammer the server for sweet sweet data" \
--kind "client" \
--tp-carrier $carrier \
--verbose \
--fail \
-- \
../otel-cli exec -n fake-server -k server /bin/echo 500 NOPE
# ^ child span, the responding "server" that just echos NOPE
================================================
FILE: demos/10-span-background-simple.sh
================================================
#!/bin/bash
# an otel-cli demo of span background
../otel-cli span background \
--service otel-cli-demo \
--name "executing $0" \
--timeout 2 &
# ^ run otel-cli in the background
sleep 1
# that's it, that's the demo
# when this script exits, otel-cli will exit too so total runtime will
# be a bit over 1 second
================================================
FILE: demos/15span-background-layered.sh
================================================
#!/bin/bash
# an otel-cli demo of span background
#
# This demo shows span background functionality with events added to the span
# while it's running in the background, then a child span is created and
# the background span is ended gracefully.
set -e
set -x
carrier=$(mktemp) # traceparent propagation via tempfile
sockdir=$(mktemp -d) # a unix socket will be created here
export OTEL_SERVICE_NAME="otel-cli-demo"
# start the span background server, set up trace propagation, and
# time out after 10 seconds (which shouldn't be reached)
../otel-cli span background \
--verbose --fail \
--tp-carrier $carrier \
--sockdir $sockdir \
--tp-print \
--name "$0 script execution" \
--timeout 10 &
data1=$(uuidgen)
# add an event to the span running in the background, with an attribute
# set to the uuid we just generated
../otel-cli span event \
--verbose \
--name "did a thing" \
--sockdir $sockdir \
--attrs "data1=$data1"
# waste some time
sleep 1
# add an event that says we wasted some time
../otel-cli span event --verbose --name "slept 1 second" --sockdir $sockdir
# run a shorter sleep inside a child span, also note that this is using
# --tp-required so this will fail loudly if there is no traceparent
# available
../otel-cli exec \
--verbose --fail \
--name "sleep 0.2" \
--tp-required \
--tp-carrier $carrier \
--tp-print \
sleep 0.2
# finally, tell the background server we're all done and it can exit
../otel-cli span end --sockdir $sockdir
================================================
FILE: demos/20span-background-race-workarounds.sh
================================================
#!/bin/bash
# an otel-cli demo of workarounds for race conditions on span background
#
# otel-cli span background is usually run as a subprocess with & in the command
# as below. An issue that shows up sometimes is a race condition where the shell
# starts otel-cli in the background, and then immediately calls otel-cli span
# or similar hoping to use the --tp-carrier file, which might not be created
# before the process looks for it. There are a couple solutions below.
set -e
set -x
carrier=$(mktemp) # traceparent propagation via tempfile
sockdir=$(mktemp -d) # a unix socket will be created here
export OTEL_SERVICE_NAME="otel-cli-demo"
../otel-cli span background \
--tp-carrier $carrier \
--sockdir $sockdir \
--service otel-cli \
--name "$0 script execution #1" \
--timeout 10 &
# On Linux, the inotifywait command will do the trick, waiting for the
# file to be written. Without a timeout it could hang forever if it loses
# the race and otel-cli finishes writing the carrier before inotifywait
# starts watching. A short timeout ensures it won't hang.
[ ! -e $carrier ] && inotifywait --timeout 0.1 $carrier
../otel-cli span --tp-carrier $carrier --name "child of span background, after inotifywait"
../otel-cli span end --sockdir $sockdir
# start another one for the second example
../otel-cli span background \
--tp-carrier $carrier \
--sockdir $sockdir \
--service otel-cli \
--name "$0 script execution #2" \
--timeout 10 &
# otel-cli span event already waits for span background's socket file
# to appear so just sending an event will do enough synchronization, at
# the cost of a meaningless event.
../otel-cli span event --sockdir $sockdir --name "wait for span background"
../otel-cli span --tp-carrier $carrier --name "child of span background, after span event"
../otel-cli span end --sockdir $sockdir
================================================
FILE: demos/25srecon22-talk-agenda.sh
================================================
#!/bin/bash
# a quick script to render a talk agenda using OTel
svc="SRECon"
# turns out the date doesn't matter much for this exercise since
# we don't show it
# but it is important for /finding/ these spans once you've sent them
# so use today's date and hour but munge the rest
talk_start=$(date +%Y-%m-%dT%H:00:00.00000Z)
talk_end=$(date +%Y-%m-%dT%H:00:40.00000Z)
tpfile=$(mktemp)
otel-cli span \
--name "OpenTelemetry::Trace.tracer(BareMetal)" \
--service "$svc" \
--start "$talk_start" \
--end "$talk_end" \
--tp-print \
--tp-export \
--tp-carrier $tpfile
# set the traceparent
. $tpfile
otel-cli span \
--name "Agenda" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:01:00/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:02:00/")"
subtpfile=$(mktemp)
otel-cli span \
--name "The Bad Old Days" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:02:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:03:01/")"
subtpfile=$(mktemp)
otel-cli span \
--name "Tracing All the Things!" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:03:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:10:01/")" \
--tp-export \
--tp-carrier $subtpfile
. $subtpfile # next spans are under ^^
otel-cli span \
--name "OTel in the Metal API" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:03:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:04:00/")"
otel-cli span \
--name "Fits and Starts" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:04:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:07:00/")"
otel-cli span \
--name "Introducing the Metal SRE team" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:07:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:10:00/")"
. $tpfile # back to top trace
subtpfile=$(mktemp)
otel-cli span \
--name "Observability Onboarding" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:10:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:15:01/")" \
--tp-export \
--tp-carrier $subtpfile
. $subtpfile # next spans are under ^^
otel-cli span \
--name "Order of Operations" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:10:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:12:00/")"
otel-cli span \
--name "Mental Models" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:12:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:14:00/")"
otel-cli span \
--name "How We Know We're on the Right Track" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:14:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:16:00/")"
. $tpfile # back to top trace
subtpfile=$(mktemp)
otel-cli span \
--name "Tracing Wins" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:16:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:22:01/")" \
--tp-export \
--tp-carrier $subtpfile
. $subtpfile # next spans are under ^^
otel-cli span \
--name "Performance Project" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:16:02/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:19:00/")"
otel-cli span \
--name "Sociotechnical Wins" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:19:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:22:00/")"
. $tpfile # back to top trace
otel-cli span \
--name "Tracing Bear Metal" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:22:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:28:00/")"
otel-cli span \
--name "Recap" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:28:01/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:29:59/")"
otel-cli span \
--name "Q & A" \
--service "$svc" \
--start "$(echo "$talk_start" |sed "s/:00:00/:30:00/")" \
--end "$(echo "$talk_start" |sed "s/:00:00/:40:00/")"
================================================
FILE: demos/30-trace-build-process/otel-wrapper-shim.sh
================================================
#!/bin/bash
# Build Process Tracing Example
#
# It's possible to instrument complex build processes in nodejs (npm/yarn/pnpm) and
# C (make/cmake/ninja) by injecting instrumented versions of some key commands into $PATH.
#
# This example is a folder with a shim script in it, and a bunch of fake tools that are just
# symlinks to the shim. It needs to be a folder so that it can be cleanly injected into $PATH.
#
# To see this working, start `otel-desktop-viewer` as discribed in the main README.md, and then:
# (
# cd .. && \
# ([ -d mermaid ] || git clone https://github.com/mermaid-js/mermaid) && \
# cd mermaid/ && \
# ../otel-cli/demos/30-trace-build-process/pnpm install
# )
#
# For more complex build processes, I have found jaeger to be quite good, because it also includes
# a black "critical path line". See https://www.jaegertracing.io/docs/1.54/getting-started/ for
# their all-in-one docker conainer setup instructions. Otherwise, everything else is the same as
# when using otel-desktop-viewer.
set -euo pipefail
export OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-localhost:4317}"
TOOL_NAME="$(basename $0)"
LOCATION_IN_PATH="$(dirname $0)"
HERE="$(dirname $(readlink -f $0))"
# This is just a guess, based on what's left after we've removed ourselves and any symlinks we know about from the results.
# If we're symlinked into $PATH in more than once place then we could still end up in loops.
# If this becomes a problem, we could instead iterate over each location and call readlink,
# and drop anything that resolves to $HERE.
ORIGINAL_TOOL_PATH="$(which -a "$TOOL_NAME" | grep -Ev "($HERE|$LOCATION_IN_PATH)" | head -n1)"
# Put this dir first in $PATH so that nested calls out to bash and pnpm are instrumented
# (tools like npm have a habit of messing with $PATH to put themselves first in subshells).
# This will probably get quite long if there is a lot of recursion.
# If this causes problems, we could prune ouselves out before inserting ourself at the head.
export PATH="$HERE:$PATH"
otel-cli exec --service "$TOOL_NAME" --name "$*" -- $ORIGINAL_TOOL_PATH "$@"
================================================
FILE: docker-compose.yaml
================================================
---
version: '2.1'
services:
jaeger:
image: jaegertracing/all-in-one:1.58.0
ports:
- "16686:16686"
- "14268"
- "14250"
otel-collector:
image: otel/opentelemetry-collector:0.76.1
volumes:
- ./configs/otel-collector.yaml:/local.yaml
command: --config /local.yaml
ports:
- "4317:4317"
depends_on:
- jaeger
================================================
FILE: example-config.json
================================================
{
"endpoint" : "localhost:4317",
"traces_endpoint": "",
"timeout" : "1s",
"otlp_headers" : {
"header1" : "header1-value"
},
"otlp_blocking" : false,
"insecure" : true,
"tls_no_verify" : true,
"tls_ca_cert": "/dev/null",
"tls_client_key": "/dev/null",
"tls_client_cert": "/dev/null",
"service_name" : "configured_in_config_file",
"span_name" : "config_file_span",
"span_kind" : "server",
"span_attributes" : {
"attr1" : "value1"
},
"span_end_time" : "now",
"span_start_time" : "now",
"span_status_code" : "0",
"span_status_description" : "status description",
"event_name" : "config_file_event",
"event_time" : "now",
"background_parent_poll_ms" : 100,
"background_socket_directory" : "/tmp",
"background_wait" : true,
"traceparent_carrier_file" : "/tmp/traceparent.txt",
"traceparent_ignore_env" : true,
"traceparent_print" : true,
"traceparent_print_export" : true,
"traceparent_required" : false,
"verbose" : true,
"fail" : true
}
================================================
FILE: go.mod
================================================
module github.com/equinix-labs/otel-cli
go 1.21
toolchain go1.22.4
require (
github.com/google/go-cmp v0.6.0
github.com/pterm/pterm v0.12.79
github.com/spf13/cobra v1.8.0
go.opentelemetry.io/otel v1.27.0
go.opentelemetry.io/otel/sdk v1.27.0
go.opentelemetry.io/proto/otlp v1.1.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.2
)
require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
)
================================================
FILE: go.sum
================================================
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4=
github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 h1:9Xyg6I9IWQZhRVfCWjKK+l6kI0jHcPesVlMnT//aHNo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: main.go
================================================
package main
import (
"os"
"github.com/equinix-labs/otel-cli/otelcli"
)
// these will be set by goreleaser & ldflags at build time
var (
version = ""
commit = ""
date = ""
)
func main() {
otelcli.Execute(otelcli.FormatVersion(version, commit, date))
os.Exit(otelcli.GetExitCode())
}
================================================
FILE: main_test.go
================================================
package main_test
// This file implements the data-driven test harness for otel-cli. It executes
// tests defined in data_for_test.go, and uses the CA implementation in
// tls_for_test.go.
//
// see TESTING.md for details
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"log"
"net"
"os"
"os/exec"
"regexp"
"strings"
"testing"
"time"
"github.com/equinix-labs/otel-cli/otlpclient"
"github.com/equinix-labs/otel-cli/otlpserver"
"github.com/google/go-cmp/cmp"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
)
// otel-cli will fail with "getent not found" if PATH is empty
// so set it to the bare minimum and always the same for cleanup
const minimumPath = `/bin:/usr/bin`
const defaultTestTimeout = time.Second
func TestMain(m *testing.M) {
// wipe out this process's envvars right away to avoid pollution & leakage
os.Clearenv()
result := m.Run()
os.Exit(result)
}
// TestOtelCli iterates over all defined fixtures and executes the tests.
func TestOtelCli(t *testing.T) {
_, err := os.Stat("./otel-cli")
if os.IsNotExist(err) {
t.Fatalf("otel-cli must be built and present as ./otel-cli for this test suite to work (try: go build)")
}
var fixtureCount int
for _, suite := range suites {
fixtureCount += len(suite)
for i, fixture := range suite {
// clean up some nils here so the test data can be a bit more terse
if fixture.Config.CliArgs == nil {
suite[i].Config.CliArgs = []string{}
}
if fixture.Config.Env == nil {
suite[i].Config.Env = map[string]string{}
}
if fixture.Expect.Env == nil {
suite[i].Expect.Env = map[string]string{}
}
if fixture.Expect.SpanData == nil {
suite[i].Expect.SpanData = map[string]string{}
}
// make sure PATH hasn't been set, because doing that in fixtures is naughty
if _, ok := fixture.Config.Env["PATH"]; ok {
t.Fatalf("fixture in file %s is not allowed to modify or test envvar PATH", fixture.Name)
}
}
}
t.Logf("Running %d test suites and %d fixtures.", len(suites), fixtureCount)
if len(suites) == 0 || fixtureCount == 0 {
t.Fatal("no test fixtures loaded!")
}
// generates a CA, client, and server certs to use in tests
tlsData := generateTLSData(t)
defer tlsData.cleanup()
for _, suite := range suites {
// a fixture can be backgrounded after starting it up for e.g. otel-cli span background
// a second fixture with the same description later in the list will "foreground" it
bgFixtureWaits := make(map[string]chan struct{})
bgFixtureDones := make(map[string]chan struct{})
fixtures:
for _, fixture := range suite {
// some tests explicitly spend time sleeping/waiting to validate timeouts are working
// and when they are marked as such, they can be skipped with go test -test.short
if testing.Short() && fixture.Config.IsLongTest {
t.Skipf("[%s] skipping timeout tests in short mode", fixture.Name)
continue fixtures
}
// inject the TlsData into the fixture
fixture.TlsData = tlsData
// when a fixture is foregrounded all it does is signal the background fixture
// to finish doing its then, waits for it to finish, then continues on
if fixture.Config.Foreground {
if wait, ok := bgFixtureWaits[fixture.Name]; ok {
wait <- struct{}{}
delete(bgFixtureWaits, fixture.Name)
} else {
t.Fatalf("BUG in test or fixture: unexpected foreground fixture wait chan named %q", fixture.Name)
}
if done, ok := bgFixtureDones[fixture.Name]; ok {
<-done
delete(bgFixtureDones, fixture.Name)
} else {
t.Fatalf("BUG in test or fixture: unexpected foreground fixture done chan named %q", fixture.Name)
}
continue fixtures
}
// flow control for backgrounding fixtures:
fixtureWait := make(chan struct{})
fixtureDone := make(chan struct{})
go runFixture(t, fixture, fixtureWait, fixtureDone)
if fixture.Config.Background {
// save off the channels for flow control
t.Logf("[%s] fixture %q backgrounded", fixture.Name, fixture.Name)
bgFixtureWaits[fixture.Name] = fixtureWait
bgFixtureDones[fixture.Name] = fixtureDone
} else {
// actually the default case, just block as if the code was ran synchronously
fixtureWait <- struct{}{}
<-fixtureDone
}
}
}
}
// runFixture runs the OTLP server & command, waits for signal, checks
// results, then signals it's done.
func runFixture(t *testing.T, fixture Fixture, wait, done chan struct{}) {
// sets up an OTLP server, runs otel-cli, packages data up in these return vals
endpoint, results := runOtelCli(t, fixture)
<-wait
// inject the runtime endpoint into the fixture
fixture.Endpoint = endpoint
checkAll(t, fixture, results)
done <- struct{}{}
}
// checkAll gathers up all the check* funcs below into one function.
func checkAll(t *testing.T, fixture Fixture, results Results) {
// check timeout and process status expectations
success := checkProcess(t, fixture, results)
// when the process fails, no point in checking the rest, it's just noise
if !success {
t.Log("otel-cli process failed unexpectedly, will not test values from it")
return
}
// compare the number of recorded spans against expectations in the fixture
checkSpanCount(t, fixture, results)
// compares the data in each recorded span against expectations in the fixture
if len(fixture.Expect.SpanData) > 0 {
checkSpanData(t, fixture, results)
}
// many of the basic plumbing tests use status so it has its own set of checks
// but these shouldn't run for testing the other subcommands
if len(fixture.Config.CliArgs) > 0 && fixture.Config.CliArgs[0] == "status" && results.ExitCode == 0 {
checkStatusData(t, fixture, results)
} else {
// checking the text output only makes sense for non-status paths
checkOutput(t, fixture, results)
}
if len(fixture.Expect.Headers) > 0 {
checkHeaders(t, fixture, results)
}
if len(fixture.Expect.ServerMeta) > 0 {
checkServerMeta(t, fixture, results)
}
checkFuncs(t, fixture, results)
}
// compare the number of spans recorded by the test server against the
// number of expected spans in the fixture, if specified. If no expected
// span count is specified, this check always passes.
func checkSpanCount(t *testing.T, fixture Fixture, results Results) {
if results.SpanCount != fixture.Expect.SpanCount {
t.Errorf("[%s] span count was %d but expected %d", fixture.Name, results.SpanCount, fixture.Expect.SpanCount)
}
}
// checkProcess validates configured expectations about whether otel-cli failed
// or the test timed out. These are mostly used for testing that otel-cli fails
// in the way we want it to.
func checkProcess(t *testing.T, fixture Fixture, results Results) bool {
if results.TimedOut != fixture.Expect.TimedOut {
t.Errorf("[%s] test timeout status is %t but expected %t", fixture.Name, results.TimedOut, fixture.Expect.TimedOut)
return false
}
if results.CommandFailed != fixture.Expect.CommandFailed {
t.Errorf("[%s] command failed is %t but expected %t", fixture.Name, results.CommandFailed, fixture.Expect.CommandFailed)
return false
}
return true
}
// checkOutput looks that otel-cli output stored in the results and compares against
// the fixture expectation (with {{endpoint}} replaced).
func checkOutput(t *testing.T, fixture Fixture, results Results) {
wantOutput := injectVars(fixture.Expect.CliOutput, fixture.Endpoint, fixture.TlsData)
gotOutput := results.CliOutput
if fixture.Expect.CliOutputRe != nil {
gotOutput = fixture.Expect.CliOutputRe.ReplaceAllString(gotOutput, "")
}
if diff := cmp.Diff(wantOutput, gotOutput); diff != "" {
if fixture.Expect.CliOutputRe != nil {
t.Errorf("[%s] test fixture RE modified output from %q to %q", fixture.Name, fixture.Expect.CliOutput, gotOutput)
}
t.Errorf("[%s] otel-cli output did not match fixture (-want +got):\n%s", fixture.Name, diff)
}
}
// checkStatusData compares the sections of otel-cli status output against
// fixture data, substituting {{endpoint}} into fixture data as needed.
func checkStatusData(t *testing.T, fixture Fixture, results Results) {
// check the env
injectMapVars(fixture.Endpoint, fixture.Expect.Env, fixture.TlsData)
if diff := cmp.Diff(fixture.Expect.Env, results.Env); diff != "" {
t.Errorf("env data did not match fixture in %q (-want +got):\n%s", fixture.Name, diff)
}
// check diagnostics, use string maps so the diff output is easy to compare to json
wantDiag := fixture.Expect.Diagnostics.ToStringMap()
gotDiag := results.Diagnostics.ToStringMap()
injectMapVars(fixture.Endpoint, wantDiag, fixture.TlsData)
// there's almost always going to be cli_args in practice, so if the fixture has
// an empty string, just ignore that key
if wantDiag["cli_args"] == "" {
gotDiag["cli_args"] = ""
}
for k, v := range wantDiag {
if v == "*" {
wantDiag[k] = gotDiag[k]
}
}
if diff := cmp.Diff(wantDiag, gotDiag); diff != "" {
t.Errorf("[%s] diagnostic data did not match fixture (-want +got):\n%s", fixture.Name, diff)
}
// check the configuration
wantConf := fixture.Expect.Config.ToStringMap()
gotConf := results.Config.ToStringMap()
// if an expected config string is set to "*" it will match anything
// and is effectively ignored
for k, v := range wantConf {
if v == "*" {
// set to same so cmd.Diff will ignore
wantConf[k] = gotConf[k]
}
}
injectMapVars(fixture.Endpoint, wantConf, fixture.TlsData)
if diff := cmp.Diff(wantConf, gotConf); diff != "" {
t.Errorf("[%s] config data did not match fixture (-want +got):\n%s", fixture.Name, diff)
}
}
// checkSpanData compares the data in spans received from otel-cli against the
// fixture data.
func checkSpanData(t *testing.T, fixture Fixture, results Results) {
// check the expected span data against what was received by the OTLP server
gotSpan := otlpclient.SpanToStringMap(results.Span, results.ResourceSpans)
injectMapVars(fixture.Endpoint, gotSpan, fixture.TlsData)
wantSpan := map[string]string{} // to be passed to cmp.Diff
// verify all fields that were expected were present in output span
for what := range fixture.Expect.SpanData {
if _, ok := gotSpan[what]; !ok {
t.Errorf("[%s] expected span to have key %q but it is not present", fixture.Name, what)
}
}
// spanRegexChecks is a map of field names and regexes for basic presence
// and validity checking of span data fields with expected set to "*"
spanRegexChecks := map[string]*regexp.Regexp{
"trace_id": regexp.MustCompile(`^[0-9a-fA-F]{32}$`),
"span_id": regexp.MustCompile(`^[0-9a-fA-F]{16}$`),
"name": regexp.MustCompile(`^\w+$`),
"parent": regexp.MustCompile(`^[0-9a-fA-F]{32}$`),
"kind": regexp.MustCompile(`^\w+$`), // TODO: can validate more here
"start": regexp.MustCompile(`^\d+$`),
"end": regexp.MustCompile(`^\d+$`),
"attributes": regexp.MustCompile(`\w+=.+`), // not great but should validate at least one k=v
}
// go over all the keys in the received span and check against expected values
// in the fixture, and the spanRegexChecks
// modifies the gotSpan/wantSpan maps so cmp.Diff can do its thing
for what, gotVal := range gotSpan {
var wantVal string
var ok bool
if wantVal, ok = fixture.Expect.SpanData[what]; ok {
wantSpan[what] = wantVal
} else {
wantSpan[what] = gotVal // make a straight copy to make cmp.Diff happy
}
if re, ok := spanRegexChecks[what]; ok {
if wantVal == "*" {
// * means if the above RE returns cleanly then pass the test
if re.MatchString(gotVal) {
delete(gotSpan, what)
delete(wantSpan, what)
} else {
t.Errorf("[%s] span value %q for key %s is not valid", fixture.Name, gotVal, what)
}
}
}
}
injectMapVars(fixture.Endpoint, wantSpan, fixture.TlsData)
// a regular expression can be put in e.g. /^foo$/ to get evaluated as RE
for key, wantVal := range wantSpan {
if strings.HasPrefix(wantVal, "/") && strings.HasSuffix(wantVal, "/") {
re := regexp.MustCompile(wantVal[1 : len(wantVal)-1]) // slice strips the /'s off
if !re.MatchString(gotSpan[key]) {
t.Errorf("regular expression %q did not match on %q", wantVal, gotSpan[key])
}
delete(gotSpan, key) // delete from both maps so cmp.Diff ignores them
delete(wantSpan, key)
}
}
// do a diff on a generated map that sets values to * when the * check succeeded
if diff := cmp.Diff(wantSpan, gotSpan); diff != "" {
t.Errorf("[%s] otel span info did not match fixture (-want +got):\n%s", fixture.Name, diff)
}
}
// checkHeaders compares the expected and received headers.
func checkHeaders(t *testing.T, fixture Fixture, results Results) {
// gzip encoding makes automatically comparing values tricky, so ignore it
// unless the test actually specifies a length
if _, ok := fixture.Expect.Headers["Content-Length"]; !ok {
delete(results.Headers, "Content-Length")
}
injectMapVars(fixture.Endpoint, fixture.Expect.Headers, fixture.TlsData)
injectMapVars(fixture.Endpoint, results.Headers, fixture.TlsData)
for k, v := range fixture.Expect.Headers {
if v == "*" {
// overwrite value so cmp.Diff will ignore
results.Headers[k] = "*"
}
}
if diff := cmp.Diff(fixture.Expect.Headers, results.Headers); diff != "" {
t.Errorf("[%s] headers did not match expected (-want +got):\n%s", fixture.Name, diff)
}
}
// checkServerMeta compares the expected and received server metadata.
func checkServerMeta(t *testing.T, fixture Fixture, results Results) {
injectMapVars(fixture.Endpoint, fixture.Expect.ServerMeta, fixture.TlsData)
injectMapVars(fixture.Endpoint, results.ServerMeta, fixture.TlsData)
if diff := cmp.Diff(fixture.Expect.ServerMeta, results.ServerMeta); diff != "" {
t.Errorf("[%s] server metadata did not match expected (-want +got):\n%s", fixture.Name, diff)
}
}
// checkFuncs runs through the list of funcs in the fixture to do
// complex checking on data. Funcs should call t.Errorf, etc. as needed.
func checkFuncs(t *testing.T, fixture Fixture, results Results) {
for _, f := range fixture.CheckFuncs {
f(t, fixture, results)
}
}
// runOtelCli runs the a server and otel-cli together and captures their
// output as data to return for further testing.
func runOtelCli(t *testing.T, fixture Fixture) (string, Results) {
started := time.Now()
results := Results{
SpanData: map[string]string{},
Env: map[string]string{},
SpanEvents: []*tracepb.Span_Event{},
}
// these channels need to be buffered or the callback will hang trying to send
rcvSpan := make(chan *tracepb.Span, 100) // 100 spans is enough for anybody
rcvEvents := make(chan []*tracepb.Span_Event, 100)
// otlpserver calls this function for each span received
cb := func(ctx context.Context, span *tracepb.Span, events []*tracepb.Span_Event, rss *tracepb.ResourceSpans, headers map[string]string, meta map[string]string) bool {
rcvSpan <- span
rcvEvents <- events
results.ServerMeta = meta
results.ResourceSpans = rss
results.SpanCount++
results.EventCount += len(events)
results.Headers = headers
// true tells the server we're done and it can exit its loop
return results.SpanCount >= fixture.Expect.SpanCount
}
var cs otlpserver.OtlpServer
switch fixture.Config.ServerProtocol {
case grpcProtocol:
cs = otlpserver.NewServer("grpc", cb, func(otlpserver.OtlpServer) {})
case httpProtocol:
cs = otlpserver.NewServer("http", cb, func(otlpserver.OtlpServer) {})
}
defer cs.Stop()
serverTimeout := time.Duration(fixture.Config.TestTimeoutMs) * time.Millisecond
if serverTimeout == time.Duration(0) {
serverTimeout = defaultTestTimeout
}
// start a timeout goroutine for the otlp server, cancelable with done<-struct{}{}
cancelServerTimeout := make(chan struct{}, 1)
go func() {
select {
case <-time.After(serverTimeout):
results.TimedOut = true
cs.Stop() // supports multiple calls
case <-cancelServerTimeout:
return
}
}()
// port :0 means randomly assigned port, which we copy into {{endpoint}}
var listener net.Listener
var err error
if fixture.Config.ServerTLSEnabled {
tlsConf := fixture.TlsData.serverTLSConf.Clone()
if fixture.Config.ServerTLSAuthEnabled {
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
}
listener, err = tls.Listen("tcp", "localhost:0", tlsConf)
} else {
listener, err = net.Listen("tcp", "localhost:0")
}
endpoint := listener.Addr().String()
if err != nil {
// t.Fatalf is not allowed since we run this in a goroutine
t.Errorf("[%s] failed to listen on OTLP endpoint %q: %s", fixture.Name, endpoint, err)
return endpoint, results
}
t.Logf("[%s] starting OTLP server on %q", fixture.Name, endpoint)
// TODO: might be neat to have a mode where we start the listener and do nothing
// with it to simulate a hung server or opentelemetry-collector
go func() {
cs.Serve(listener)
}()
// let things go this far to generate the endpoint port then stop the server before
// calling otel-cli so we can test timeouts
if fixture.Config.StopServerBeforeExec {
cs.Stop()
listener.Close()
}
// TODO: figure out the best way to build the binary and detect if the build is stale
// ^^ probably doesn't matter much in CI, can auto-build, but for local workflow it matters
// TODO: should all otel-cli commands be able to dump status? e.g. otel-cli span --status
args := []string{}
if len(fixture.Config.CliArgs) > 0 {
for _, v := range fixture.Config.CliArgs {
args = append(args, injectVars(v, endpoint, fixture.TlsData))
}
}
statusCmd := exec.Command("./otel-cli", args...)
statusCmd.Env = mkEnviron(endpoint, fixture.Config.Env, fixture.TlsData)
// have command write output into string buffers
var cliOut bytes.Buffer
statusCmd.Stdout = &cliOut
statusCmd.Stderr = &cliOut
err = statusCmd.Start()
if err != nil {
t.Fatalf("[%s] error starting otel-cli: %s", fixture.Name, err)
}
stopKiller := make(chan struct{}, 1)
if fixture.Config.KillAfter != 0 {
go func() {
select {
case <-time.After(fixture.Config.KillAfter):
err := statusCmd.Process.Signal(fixture.Config.KillSignal)
if err != nil {
log.Fatalf("[%s] error sending signal %s to pid %d: %s", fixture.Name, fixture.Config.KillSignal, statusCmd.Process.Pid, err)
}
case <-stopKiller:
return
}
}()
} else {
go func() {
select {
case <-time.After(serverTimeout):
t.Logf("[%s] timeout, killing process...", fixture.Name)
results.TimedOut = true
err = statusCmd.Process.Kill()
if err != nil {
// TODO: this might be a bit fragile, soften this up later if it ends up problematic
log.Fatalf("[%s] %d timeout process kill failed: %s", fixture.Name, serverTimeout, err)
}
case <-stopKiller:
return
}
}()
}
// grab stderr & stdout comingled so that if otel-cli prints anything to either it's not
// supposed to it will cause e.g. status json parsing and other tests to fail
t.Logf("[%s] going to exec 'env -i %s %s'", fixture.Name, strings.Join(statusCmd.Env, " "), strings.Join(statusCmd.Args, " "))
err = statusCmd.Wait()
results.CliOutput = cliOut.String()
results.ExitCode = statusCmd.ProcessState.ExitCode()
results.CommandFailed = !statusCmd.ProcessState.Exited()
if err != nil {
t.Logf("[%s] command exited: %s", fixture.Name, err)
}
// send stop signals to the timeouts and OTLP server
cancelServerTimeout <- struct{}{}
stopKiller <- struct{}{}
cs.Stop()
// only try to parse status json if it was a status command
// TODO: support variations on otel-cli where status isn't the first arg
if len(args) > 0 && args[0] == "status" && results.ExitCode == 0 {
err = json.Unmarshal(cliOut.Bytes(), &results)
if err != nil {
t.Errorf("[%s] parsing otel-cli status output failed: %s", fixture.Name, err)
t.Logf("[%s] output received: %q", fixture.Name, cliOut)
return endpoint, results
}
// remove PATH from the output but only if it's exactly what we set on exec
if path, ok := results.Env["PATH"]; ok {
if path == minimumPath {
delete(results.Env, "PATH")
}
}
}
// when no spans are expected, return without reading from the channels
if fixture.Expect.SpanCount == 0 {
return endpoint, results
}
// grab the spans & events from the server off the channels it writes to
remainingTimeout := serverTimeout - time.Since(started)
var gatheredSpans int
gather:
for {
select {
case <-time.After(remainingTimeout):
break gather
case results.Span = <-rcvSpan:
// events is always populated at the same time as the span is sent
// and will always send at least an empty list
results.SpanEvents = <-rcvEvents
// with this approach, any mismatch in spans produced and expected results
// in a timeout with the above time.After
gatheredSpans++
if gatheredSpans == results.SpanCount {
// TODO: it would be slightly nicer to use plural.Selectf instead of 'span(s)'
t.Logf("[%s] test gathered %d span(s)", fixture.Name, gatheredSpans)
break gather
}
}
}
return endpoint, results
}
// mkEnviron converts a string map to a list of k=v strings and tacks on PATH.
func mkEnviron(endpoint string, env map[string]string, tlsData TlsSettings) []string {
mapped := make([]string, len(env)+1)
var i int
for k, v := range env {
mapped[i] = k + "=" + injectVars(v, endpoint, tlsData)
i++
}
// always tack on a PATH otherwise the binary will fail with no PATH
// to get to getent(1)
mapped[len(mapped)-1] = "PATH=" + minimumPath
return mapped
}
// injectMapVars iterates over the map and updates the values, replacing all instances
// of {{endpoint}}, {{tls_ca_cert}}, {{tls_client_cert}}, and {{tls_client_key}} with
// test values.
func injectMapVars(endpoint string, target map[string]string, tlsData TlsSettings) {
for k, v := range target {
target[k] = injectVars(v, endpoint, tlsData)
}
}
// injectVars replaces all instances of {{endpoint}}, {{tls_ca_cert}},
// {{tls_client_cert}}, and {{tls_client_key}} with test values.
// This is needed because the otlpserver is configured to listen on :0 which has it
// grab a random port. Once that's generated we need to inject it into all the values
// so the test comparisons work as expected. Similarly for TLS testing, a temp CA and
// certs are created and need to be injected.
func injectVars(in, endpoint string, tlsData TlsSettings) string {
out := strings.ReplaceAll(in, "{{endpoint}}", endpoint)
out = strings.ReplaceAll(out, "{{tls_ca_cert}}", tlsData.caFile)
out = strings.ReplaceAll(out, "{{tls_client_cert}}", tlsData.clientFile)
out = strings.ReplaceAll(out, "{{tls_client_key}}", tlsData.clientPrivKeyFile)
return out
}
================================================
FILE: otelcli/completion.go
================================================
package otelcli
import (
"log"
"os"
"github.com/spf13/cobra"
)
func completionCmd(*Config) *cobra.Command {
cmd := cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
$ source <(otel-cli completion bash)
# To load completions for each session, execute once:
# Linux:
$ otel-cli completion bash > /etc/bash_completion.d/otel-cli
# macOS:
$ otel-cli completion bash > /usr/local/etc/bash_completion.d/otel-cli
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ otel-cli completion zsh > "${fpath[1]}/_otel-cli"
# You will need to start a new shell for this setup to take effect.
fish:
$ otel-cli completion fish | source
# To load completions for each session, execute once:
$ otel-cli completion fish > ~/.config/fish/completions/otel-cli.fish
PowerShell:
PS> otel-cli completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> otel-cli completion powershell > otel-cli.ps1
# and source this file from your PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1)),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
err := cmd.Root().GenBashCompletion(os.Stdout)
if err != nil {
log.Fatalf("failed to write completion to stdout")
}
case "zsh":
err := cmd.Root().GenZshCompletion(os.Stdout)
if err != nil {
log.Fatalf("failed to write completion to stdout")
}
case "fish":
err := cmd.Root().GenFishCompletion(os.Stdout, true)
if err != nil {
log.Fatalf("failed to write completion to stdout")
}
case "powershell":
err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
if err != nil {
log.Fatalf("failed to write completion to stdout")
}
}
},
}
return &cmd
}
================================================
FILE: otelcli/config.go
================================================
package otelcli
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"log"
"net/url"
"os"
"path"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
var detectBrokenRFC3339PrefixRe *regexp.Regexp
var epochNanoTimeRE *regexp.Regexp
func init() {
detectBrokenRFC3339PrefixRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} `)
epochNanoTimeRE = regexp.MustCompile(`^\d+\.\d+$`)
}
// DefaultConfig returns a Config with all defaults set.
func DefaultConfig() Config {
return Config{
Endpoint: "",
Protocol: "",
Timeout: "1s",
Headers: map[string]string{},
Insecure: false,
Blocking: false,
TlsNoVerify: false,
TlsCACert: "",
TlsClientKey: "",
TlsClientCert: "",
ServiceName: "otel-cli",
SpanName: "todo-generate-default-span-names",
Kind: "client",
ForceTraceId: "",
ForceSpanId: "",
ForceParentSpanId: "",
Attributes: map[string]string{},
TraceparentCarrierFile: "",
TraceparentIgnoreEnv: false,
TraceparentPrint: false,
TraceparentPrintExport: false,
TraceparentRequired: false,
BackgroundParentPollMs: 10,
BackgroundSockdir: "",
BackgroundWait: false,
BackgroundSkipParentPidCheck: false,
ExecCommandTimeout: "",
ExecTpDisableInject: false,
StatusCanaryCount: 1,
StatusCanaryInterval: "",
SpanStartTime: "now",
SpanEndTime: "now",
EventName: "todo-generate-default-event-names",
EventTime: "now",
CfgFile: "",
Verbose: false,
Fail: false,
StatusCode: "unset",
StatusDescription: "",
Version: "unset",
}
}
// Config stores the runtime configuration for otel-cli.
// Data structure is public so that it can serialize to json easily.
type Config struct {
Endpoint string `json:"endpoint" env:"OTEL_EXPORTER_OTLP_ENDPOINT"`
TracesEndpoint string `json:"traces_endpoint" env:"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"`
Protocol string `json:"protocol" env:"OTEL_EXPORTER_OTLP_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"`
Timeout string `json:"timeout" env:"OTEL_EXPORTER_OTLP_TIMEOUT,OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"`
Headers map[string]string `json:"otlp_headers" env:"OTEL_EXPORTER_OTLP_HEADERS"` // TODO: needs json marshaler hook to mask tokens
Insecure bool `json:"insecure" env:"OTEL_EXPORTER_OTLP_INSECURE"`
Blocking bool `json:"otlp_blocking" env:"OTEL_EXPORTER_OTLP_BLOCKING"`
TlsCACert string `json:"tls_ca_cert" env:"OTEL_EXPORTER_OTLP_CERTIFICATE,OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"`
TlsClientKey string `json:"tls_client_key" env:"OTEL_EXPORTER_OTLP_CLIENT_KEY,OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY"`
TlsClientCert string `json:"tls_client_cert" env:"OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE"`
// OTEL_CLI_NO_TLS_VERIFY is deprecated and will be removed for 1.0
TlsNoVerify bool `json:"tls_no_verify" env:"OTEL_CLI_TLS_NO_VERIFY,OTEL_CLI_NO_TLS_VERIFY"`
ServiceName string `json:"service_name" env:"OTEL_CLI_SERVICE_NAME,OTEL_SERVICE_NAME"`
SpanName string `json:"span_name" env:"OTEL_CLI_SPAN_NAME"`
Kind string `json:"span_kind" env:"OTEL_CLI_TRACE_KIND"`
Attributes map[string]string `json:"span_attributes" env:"OTEL_CLI_ATTRIBUTES"`
StatusCode string `json:"span_status_code" env:"OTEL_CLI_STATUS_CODE"`
StatusDescription string `json:"span_status_description" env:"OTEL_CLI_STATUS_DESCRIPTION"`
ForceSpanId string `json:"force_span_id" env:"OTEL_CLI_FORCE_SPAN_ID"`
ForceParentSpanId string `json:"force_parent_span_id" env:"OTEL_CLI_FORCE_PARENT_SPAN_ID"`
ForceTraceId string `json:"force_trace_id" env:"OTEL_CLI_FORCE_TRACE_ID"`
TraceparentCarrierFile string `json:"traceparent_carrier_file" env:"OTEL_CLI_CARRIER_FILE"`
TraceparentIgnoreEnv bool `json:"traceparent_ignore_env" env:"OTEL_CLI_IGNORE_ENV"`
TraceparentPrint bool `json:"traceparent_print" env:"OTEL_CLI_PRINT_TRACEPARENT"`
TraceparentPrintExport bool `json:"traceparent_print_export" env:"OTEL_CLI_EXPORT_TRACEPARENT"`
TraceparentRequired bool `json:"traceparent_required" env:"OTEL_CLI_TRACEPARENT_REQUIRED"`
BackgroundParentPollMs int `json:"background_parent_poll_ms" env:""`
BackgroundSockdir string `json:"background_socket_directory" env:""`
BackgroundWait bool `json:"background_wait" env:""`
BackgroundSkipParentPidCheck bool `json:"background_skip_parent_pid_check"`
ExecCommandTimeout string `json:"exec_command_timeout" env:"OTEL_CLI_EXEC_CMD_TIMEOUT"`
ExecTpDisableInject bool `json:"exec_tp_disable_inject" env:"OTEL_CLI_EXEC_TP_DISABLE_INJECT"`
StatusCanaryCount int `json:"status_canary_count"`
StatusCanaryInterval string `json:"status_canary_interval"`
SpanStartTime string `json:"span_start_time" env:""`
SpanEndTime string `json:"span_end_time" env:""`
EventName string `json:"event_name" env:""`
EventTime string `json:"event_time" env:""`
CfgFile string `json:"config_file" env:"OTEL_CLI_CONFIG_FILE"`
Verbose bool `json:"verbose" env:"OTEL_CLI_VERBOSE"`
Fail bool `json:"fail" env:"OTEL_CLI_FAIL"`
// not exported, used to get data from cobra to otlpclient internals
Version string `json:"-"`
}
// LoadFile reads the file specified by -c/--config and overwrites the
// current config values with any found in the file.
func (c *Config) LoadFile() error {
if c.CfgFile == "" {
return nil
}
js, err := os.ReadFile(c.CfgFile)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", c.CfgFile, err)
}
if err := json.Unmarshal(js, c); err != nil {
return fmt.Errorf("failed to parse json data in file '%s': %w", c.CfgFile, err)
}
return nil
}
// LoadEnv loads environment variables into the config, overwriting current
// values. Environment variable to config key mapping is tagged on the
// Config struct. Multiple names for envvars is supported, comma-separated.
// Takes a func(string)string that's usually os.Getenv, and is swappable to
// make testing easier.
func (c *Config) LoadEnv(getenv func(string) string) error {
// loop over each field of the Config
structType := reflect.TypeOf(c).Elem()
cValue := reflect.ValueOf(c).Elem()
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
envVars := field.Tag.Get("env")
if envVars == "" {
continue
}
// a field can have multiple comma-delimiated env vars to look in
for _, envVar := range strings.Split(envVars, ",") {
// call the provided func(string)string provided to get the
// envvar, usually os.Getenv but can be a fake for testing
envVal := getenv(envVar)
if envVal == "" {
continue
}
// type switch and write the value into the struct
target := cValue.Field(i)
switch target.Interface().(type) {
case string:
target.SetString(envVal)
case int:
intVal, err := strconv.ParseInt(envVal, 10, 64)
if err != nil {
return fmt.Errorf("could not parse %s value %q as an int: %w", envVar, envVal, err)
}
target.SetInt(intVal)
case bool:
boolVal, err := strconv.ParseBool(envVal)
if err != nil {
return fmt.Errorf("could not parse %s value %q as an bool: %w", envVar, envVal, err)
}
target.SetBool(boolVal)
case map[string]string:
mapVal, err := parseCkvStringMap(envVal)
if err != nil {
return fmt.Errorf("could not parse %s value %q as a map: %w", envVar, envVal, err)
}
mapValVal := reflect.ValueOf(mapVal)
target.Set(mapValVal)
}
}
}
return nil
}
// ToStringMap flattens the configuration into a stringmap that is easy to work
// with in tests especially with cmp.Diff. See test_main.go.
func (c Config) ToStringMap() map[string]string {
return map[string]string{
"endpoint": c.Endpoint,
"protocol": c.Protocol,
"timeout": c.Timeout,
"headers": flattenStringMap(c.Headers, "{}"),
"insecure": strconv.FormatBool(c.Insecure),
"blocking": strconv.FormatBool(c.Blocking),
"tls_no_verify": strconv.FormatBool(c.TlsNoVerify),
"tls_ca_cert": c.TlsCACert,
"tls_client_key": c.TlsClientKey,
"tls_client_cert": c.TlsClientCert,
"service_name": c.ServiceName,
"span_name": c.SpanName,
"span_kind": c.Kind,
"span_attributes": flattenStringMap(c.Attributes, "{}"),
"span_status_code": c.StatusCode,
"span_status_description": c.StatusDescription,
"traceparent_carrier_file": c.TraceparentCarrierFile,
"traceparent_ignore_env": strconv.FormatBool(c.TraceparentIgnoreEnv),
"traceparent_print": strconv.FormatBool(c.TraceparentPrint),
"traceparent_print_export": strconv.FormatBool(c.TraceparentPrintExport),
"traceparent_required": strconv.FormatBool(c.TraceparentRequired),
"background_parent_poll_ms": strconv.Itoa(c.BackgroundParentPollMs),
"background_socket_directory": c.BackgroundSockdir,
"background_wait": strconv.FormatBool(c.BackgroundWait),
"background_skip_pid_check": strconv.FormatBool(c.BackgroundSkipParentPidCheck),
"exec_command_timeout": c.ExecCommandTimeout,
"exec_tp_disable_inject": strconv.FormatBool(c.ExecTpDisableInject),
"span_start_time": c.SpanStartTime,
"span_end_time": c.SpanEndTime,
"event_name": c.EventName,
"event_time": c.EventTime,
"config_file": c.CfgFile,
"verbose": strconv.FormatBool(c.Verbose),
}
}
// GetIsRecording returns true if an endpoint is set and otel-cli expects to send real
// spans. Returns false if unconfigured and going to run inert.
func (c Config) GetIsRecording() bool {
if c.Endpoint == "" && c.TracesEndpoint == "" {
Diag.IsRecording = false
return false
}
Diag.IsRecording = true
return true
}
// ParseCliTimeout parses the --timeout string value to a time.Duration.
func (c Config) ParseCliTimeout() time.Duration {
out, err := parseDuration(c.Timeout)
Diag.ParsedTimeoutMs = out.Milliseconds()
c.SoftFailIfErr(err)
return out
}
// ParseExecCommandTimeout parses the --command-timeout string value to a time.Duration.
// When timeout is unspecified or 0, otel-cli will wait forever for the command to complete.
func (c Config) ParseExecCommandTimeout() time.Duration {
if c.ExecCommandTimeout == "" {
return 0
}
out, err := parseDuration(c.ExecCommandTimeout)
c.SoftFailIfErr(err)
return out
}
// ParseStatusCanaryInterval parses the --canary-interval string value to a time.Duration.
func (c Config) ParseStatusCanaryInterval() time.Duration {
out, err := parseDuration(c.StatusCanaryInterval)
c.SoftFailIfErr(err)
return out
}
// parseDuration parses a string duration into a time.Duration.
// When no duration letter is provided (e.g. ms, s, m, h), seconds are assumed.
// It logs an error and returns time.Duration(0) if the string is empty or unparseable.
func parseDuration(d string) (time.Duration, error) {
var out time.Duration
if d == "" {
out = time.Duration(0)
} else if parsed, err := time.ParseDuration(d); err == nil {
out = parsed
} else if secs, serr := strconv.ParseInt(d, 10, 0); serr == nil {
out = time.Second * time.Duration(secs)
} else {
return time.Duration(0), fmt.Errorf("unable to parse duration string %q: %w", d, err)
}
return out, nil
}
// ParseEndpoint takes the endpoint or signal endpoint, augments as needed
// (e.g. bare host:port for gRPC) and then parses as a URL.
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp
func (config Config) ParseEndpoint() (*url.URL, string) {
var endpoint, source string
var epUrl *url.URL
var err error
// signal-specific configs get precedence over general endpoint per OTel spec
if config.TracesEndpoint != "" {
endpoint = config.TracesEndpoint
source = "signal"
} else if config.Endpoint != "" {
endpoint = config.Endpoint
source = "general"
} else {
config.SoftFail("no endpoint configuration available")
}
parts := strings.Split(endpoint, ":")
// bare hostname? can only be grpc, prepend
if len(parts) == 1 {
epUrl, err = url.Parse("grpc://" + endpoint + ":4317")
if err != nil {
config.SoftFail("error parsing (assumed) gRPC bare host address '%s': %s", endpoint, err)
}
} else if len(parts) > 1 { // could be URI or host:port
// actual URIs
// grpc:// is only an otel-cli thing, maybe should drop it?
if parts[0] == "grpc" || parts[0] == "http" || parts[0] == "https" {
epUrl, err = url.Parse(endpoint)
if err != nil {
config.SoftFail("error parsing provided %s URI '%s': %s", source, endpoint, err)
}
} else {
// gRPC host:port
epUrl, err = url.Parse("grpc://" + endpoint)
if err != nil {
config.SoftFail("error parsing (assumed) gRPC host:port address '%s': %s", endpoint, err)
}
}
}
// Per spec, /v1/traces is the default, appended to any url passed
// to the general endpoint
if strings.HasPrefix(epUrl.Scheme, "http") && source != "signal" && !strings.HasSuffix(epUrl.Path, "/v1/traces") {
epUrl.Path = path.Join(epUrl.Path, "/v1/traces")
}
Diag.EndpointSource = source
Diag.Endpoint = epUrl.String()
return epUrl, source
}
// SoftLog only calls through to log if otel-cli was run with the --verbose flag.
// TODO: does it make any sense to support %w? probably yes, can clean up some
// diagnostics.Error touch points.
func (c Config) SoftLog(format string, a ...interface{}) {
if !c.Verbose {
return
}
log.Printf(format, a...)
}
// SoftLogIfErr calls SoftLog only if err != nil.
// Written as an interim step to pushing errors up the stack instead of calling
// SoftLog/SoftFail directly in methods that don't need a config handle.
func (c Config) SoftLogIfErr(err error) {
if err != nil {
c.SoftLog(err.Error())
}
}
// SoftFail calls through to softLog (which logs only if otel-cli was run with the --verbose
// flag), then immediately exits - with status -1 by default, or 1 if --fail was
// set (a la `curl --fail`)
func (c Config) SoftFail(format string, a ...interface{}) {
c.SoftLog(format, a...)
if c.Fail {
os.Exit(1)
} else {
os.Exit(0)
}
}
// SoftFailIfErr calls SoftFail only if err != nil.
// Written as an interim step to pushing errors up the stack instead of calling
// SoftLog/SoftFail directly in methods that don't need a config handle.
func (c Config) SoftFailIfErr(err error) {
if err != nil {
c.SoftFail(err.Error())
}
}
// flattenStringMap takes a string map and returns it flattened into a string with
// keys sorted lexically so it should be mostly consistent enough for comparisons
// and printing. Output is k=v,k=v style like attributes input.
func flattenStringMap(mp map[string]string, emptyValue string) string {
if len(mp) == 0 {
return emptyValue
}
var out string
keys := make([]string, len(mp)) // for sorting
var i int
for k := range mp {
keys[i] = k
i++
}
sort.Strings(keys)
for i, k := range keys {
out = out + k + "=" + mp[k]
if i == len(keys)-1 {
break
}
out = out + ","
}
return out
}
// parseCkvStringMap parses key=value,foo=bar formatted strings as a line of CSV
// and returns it as a string map.
func parseCkvStringMap(in string) (map[string]string, error) {
r := csv.NewReader(strings.NewReader(in))
pairs, err := r.Read()
if err != nil {
return map[string]string{}, err
}
out := make(map[string]string)
for _, pair := range pairs {
parts := strings.SplitN(pair, "=", 2)
if parts[0] != "" && parts[1] != "" {
out[parts[0]] = parts[1]
} else {
return map[string]string{}, fmt.Errorf("kv pair %s must be in key=value format", pair)
}
}
return out, nil
}
// ParseSpanStartTime returns config.SpanStartTime as time.Time.
func (c Config) ParseSpanStartTime() time.Time {
t, err := c.parseTime(c.SpanStartTime, "start")
c.SoftFailIfErr(err)
return t
}
// ParseSpanEndTime returns config.SpanEndTime as time.Time.
func (c Config) ParseSpanEndTime() time.Time {
t, err := c.parseTime(c.SpanEndTime, "end")
c.SoftFailIfErr(err)
return t
}
// ParsedEventTime returns config.EventTime as time.Time.
func (c Config) ParsedEventTime() time.Time {
t, err := c.parseTime(c.EventTime, "event")
c.SoftFailIfErr(err)
return t
}
// parseTime tries to parse Unix epoch, then RFC3339, both with/without nanoseconds
func (c Config) parseTime(ts, which string) (time.Time, error) {
// errors accumulate as parsing methods are attempted
// thrown away when one succeeds, joined & returned if none succeed
errs := []error{}
if ts == "now" {
return time.Now(), nil
}
// Unix epoch time
if i, err := strconv.ParseInt(ts, 10, 64); err == nil {
return time.Unix(i, 0), nil
} else {
errs = append(errs, fmt.Errorf("could not parse span %s time %q as Unix Epoch: %w", which, ts, err))
}
// date --rfc-3339 returns an invalid format for Go because it has a
// space instead of 'T' between date and time
if detectBrokenRFC3339PrefixRe.MatchString(ts) {
ts = strings.Replace(ts, " ", "T", 1)
}
// Unix epoch time with nanoseconds
if epochNanoTimeRE.MatchString(ts) {
parts := strings.Split(ts, ".")
if len(parts) == 2 {
secs, secsErr := strconv.ParseInt(parts[0], 10, 64)
nsecs, usecsErr := strconv.ParseInt(parts[1], 10, 64)
if secsErr == nil && usecsErr == nil && secs > 0 {
return time.Unix(secs, nsecs), nil
} else {
errs = append(errs, fmt.Errorf("could not parse span %s time %q as Unix Epoch: %w", which, ts, secsErr))
errs = append(errs, fmt.Errorf("could not parse span %s time %q as Unix Epoch.Nano: %w", which, ts, usecsErr))
}
}
}
// try RFC3339 then again with nanos
t, err := time.Parse(time.RFC3339, ts)
if err != nil {
// try again with nanos
if t, innerErr := time.Parse(time.RFC3339Nano, ts); innerErr == nil {
return t, nil
} else {
errs = append(errs, fmt.Errorf("could not parse span %s time %q as RFC3339 %w", which, ts, err))
errs = append(errs, fmt.Errorf("could not parse span %s time %q as RFC3339Nano %w", which, ts, innerErr))
}
} else {
return t, nil
}
errs = append(errs, fmt.Errorf("could not parse span %s time %q as any supported format", which, ts))
return time.Time{}, errors.Join(errs...)
}
func (c Config) GetEndpoint() *url.URL {
ep, _ := c.ParseEndpoint()
return ep
}
// WithEndpoint returns the config with Endpoint set to the provided value.
func (c Config) WithEndpoint(with string) Config {
c.Endpoint = with
return c
}
// WithTracesEndpoint returns the config with TracesEndpoint set to the provided value.
func (c Config) WithTracesEndpoint(with string) Config {
c.TracesEndpoint = with
return c
}
// WithProtocol returns the config with protocol set to the provided value.
func (c Config) WithProtocol(with string) Config {
c.Protocol = with
return c
}
// GetTimeout returns the parsed --timeout value as a time.Duration.
func (c Config) GetTimeout() time.Duration {
return c.ParseCliTimeout()
}
// WithTimeout returns the config with Timeout set to the provided value.
func (c Config) WithTimeout(with string) Config {
c.Timeout = with
return c
}
// GetHeaders returns the stringmap of configured headers.
func (c Config) GetHeaders() map[string]string {
return c.Headers
}
// WithHeades returns the config with Heades set to the provided value.
func (c Config) WithHeaders(with map[string]string) Config {
c.Headers = with
return c
}
// WithInsecure returns the config with Insecure set to the provided value.
func (c Config) WithInsecure(with bool) Config {
c.Insecure = with
return c
}
// WithBlocking returns the config with Blocking set to the provided value.
func (c Config) WithBlocking(with bool) Config {
c.Blocking = with
return c
}
// WithTlsNoVerify returns the config with NoTlsVerify set to the provided value.
func (c Config) WithTlsNoVerify(with bool) Config {
c.TlsNoVerify = with
return c
}
// WithTlsCACert returns the config with TlsCACert set to the provided value.
func (c Config) WithTlsCACert(with string) Config {
c.TlsCACert = with
return c
}
// WithTlsClientKey returns the config with NoTlsClientKey set to the provided value.
func (c Config) WithTlsClientKey(with string) Config {
c.TlsClientKey = with
return c
}
// WithTlsClientCert returns the config with NoTlsClientCert set to the provided value.
func (c Config) WithTlsClientCert(with string) Config {
c.TlsClientCert = with
return c
}
// GetServiceName returns the configured OTel service name.
func (c Config) GetServiceName() string {
return c.ServiceName
}
// WithServiceName returns the config with ServiceName set to the provided value.
func (c Config) WithServiceName(with string) Config {
c.ServiceName = with
return c
}
// WithSpanName returns the config with SpanName set to the provided value.
func (c Config) WithSpanName(with string) Config {
c.SpanName = with
return c
}
// WithKind returns the config with Kind set to the provided value.
func (c Config) WithKind(with string) Config {
c.Kind = with
return c
}
// WithAttributes returns the config with Attributes set to the provided value.
func (c Config) WithAttributes(with map[string]string) Config {
c.Attributes = with
return c
}
// WithStatusCode returns the config with StatusCode set to the provided value.
func (c Config) WithStatusCode(with string) Config {
c.StatusCode = with
return c
}
// WithStatusDescription returns the config with StatusDescription set to the provided value.
func (c Config) WithStatusDescription(with string) Config {
c.StatusDescription = with
return c
}
// WithTraceparentCarrierFile returns the config with TraceparentCarrierFile set to the provided value.
func (c Config) WithTraceparentCarrierFile(with string) Config {
c.TraceparentCarrierFile = with
return c
}
// WithTraceparentIgnoreEnv returns the config with TraceparentIgnoreEnv set to the provided value.
func (c Config) WithTraceparentIgnoreEnv(with bool) Config {
c.TraceparentIgnoreEnv = with
return c
}
// WithTraceparentPrint returns the config with TraceparentPrint set to the provided value.
func (c Config) WithTraceparentPrint(with bool) Config {
c.TraceparentPrint = with
return c
}
// WithTraceparentPrintExport returns the config with TraceparentPrintExport set to the provided value.
func (c Config) WithTraceparentPrintExport(with bool) Config {
c.TraceparentPrintExport = with
return c
}
// WithTraceparentRequired returns the config with TraceparentRequired set to the provided value.
func (c Config) WithTraceparentRequired(with bool) Config {
c.TraceparentRequired = with
return c
}
// WithBackgroundParentPollMs returns the config with BackgroundParentPollMs set to the provided value.
func (c Config) WithBackgroundParentPollMs(with int) Config {
c.BackgroundParentPollMs = with
return c
}
// WithBackgroundSockdir returns the config with BackgroundSockdir set to the provided value.
func (c Config) WithBackgroundSockdir(with string) Config {
c.BackgroundSockdir = with
return c
}
// WithBackgroundWait returns the config with BackgroundWait set to the provided value.
func (c Config) WithBackgroundWait(with bool) Config {
c.BackgroundWait = with
return c
}
// WithBackgroundSkipParentPidCheck returns the config with BackgroundSkipParentPidCheck set to the provided value.
func (c Config) WithBackgroundSkipParentPidCheck(with bool) Config {
c.BackgroundSkipParentPidCheck = with
return c
}
// WithStatusCanaryCount returns the config with StatusCanaryCount set to the provided value.
func (c Config) WithStatusCanaryCount(with int) Config {
c.StatusCanaryCount = with
return c
}
// WithStatusCanaryInterval returns the config with StatusCanaryInterval set to the provided value.
func (c Config) WithStatusCanaryInterval(with string) Config {
c.StatusCanaryInterval = with
return c
}
// WithSpanStartTime returns the config with SpanStartTime set to the provided value.
func (c Config) WithSpanStartTime(with string) Config {
c.SpanStartTime = with
return c
}
// WithSpanEndTime returns the config with SpanEndTime set to the provided value.
func (c Config) WithSpanEndTime(with string) Config {
c.SpanEndTime = with
return c
}
// WithEventName returns the config with EventName set to the provided value.
func (c Config) WithEventName(with string) Config {
c.EventName = with
return c
}
// WithEventTIme returns the config with EventTIme set to the provided value.
func (c Config) WithEventTime(with string) Config {
c.EventTime = with
return c
}
// WithCfgFile returns the config with CfgFile set to the provided value.
func (c Config) WithCfgFile(with string) Config {
c.CfgFile = with
return c
}
// WithVerbose returns the config with Verbose set to the provided value.
func (c Config) WithVerbose(with bool) Config {
c.Verbose = with
return c
}
// WithFail returns the config with Fail set to the provided value.
func (c Config) WithFail(with bool) Config {
c.Fail = with
return c
}
// Version returns the program version stored in the config.
func (c Config) GetVersion() string {
return c.Version
}
// WithVersion returns the config with Version set to the provided value.
func (c Config) WithVersion(with string) Config {
c.Version = with
return c
}
================================================
FILE: otelcli/config_span.go
================================================
package otelcli
import (
"encoding/hex"
"fmt"
"io"
"time"
"github.com/equinix-labs/otel-cli/otlpclient"
"github.com/equinix-labs/otel-cli/w3c/traceparent"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
)
// NewProtobufSpan creates a new span and populates it with information
// from the config struct.
func (c Config) NewProtobufSpan() *tracepb.Span {
span := otlpclient.NewProtobufSpan()
if c.GetIsRecording() {
span.TraceId = otlpclient.GenerateTraceId()
span.SpanId = otlpclient.GenerateSpanId()
}
span.Name = c.SpanName
span.Kind = otlpclient.SpanKindStringToInt(c.Kind)
span.Attributes = otlpclient.StringMapAttrsToProtobuf(c.Attributes)
now := time.Now()
if c.SpanStartTime != "" {
st := c.ParseSpanStartTime()
span.StartTimeUnixNano = uint64(st.UnixNano())
} else {
span.StartTimeUnixNano = uint64(now.UnixNano())
}
if c.SpanEndTime != "" {
et := c.ParseSpanEndTime()
span.EndTimeUnixNano = uint64(et.UnixNano())
} else {
span.EndTimeUnixNano = uint64(now.UnixNano())
}
if c.GetIsRecording() {
tp := c.LoadTraceparent()
if tp.Initialized {
span.TraceId = tp.TraceId
span.ParentSpanId = tp.SpanId
}
} else {
span.TraceId = otlpclient.GetEmptyTraceId()
span.SpanId = otlpclient.GetEmptySpanId()
}
// --force-trace-id, --force-span-id and --force-parent-span-id let the user set their own trace, span & parent span ids
// these work in non-recording mode and will stomp trace id from the traceparent
var err error
if c.ForceTraceId != "" {
span.TraceId, err = parseHex(c.ForceTraceId, 16)
c.SoftFailIfErr(err)
}
if c.ForceSpanId != "" {
span.SpanId, err = parseHex(c.ForceSpanId, 8)
c.SoftFailIfErr(err)
}
if c.ForceParentSpanId != "" {
span.ParentSpanId, err = parseHex(c.ForceParentSpanId, 8)
c.SoftFailIfErr(err)
}
otlpclient.SetSpanStatus(span, c.StatusCode, c.StatusDescription)
return span
}
// LoadTraceparent follows otel-cli's loading rules, start with envvar then file.
// If both are set, the file will override env.
// When in non-recording mode, the previous traceparent will be returned if it's
// available, otherwise, a zero-valued traceparent is returned.
func (c Config) LoadTraceparent() traceparent.Traceparent {
tp := traceparent.Traceparent{
Version: 0,
TraceId: otlpclient.GetEmptyTraceId(),
SpanId: otlpclient.GetEmptySpanId(),
Sampling: false,
Initialized: true,
}
if !c.TraceparentIgnoreEnv {
var err error
tp, err = traceparent.LoadFromEnv()
if err != nil {
Diag.Error = err.Error()
}
}
if c.TraceparentCarrierFile != "" {
fileTp, err := traceparent.LoadFromFile(c.TraceparentCarrierFile)
if err != nil {
Diag.Error = err.Error()
} else if fileTp.Initialized {
tp = fileTp
}
}
if c.TraceparentRequired {
if tp.Initialized {
return tp
} else {
c.SoftFail("failed to find a valid traceparent carrier in either environment for file '%s' while it's required by --tp-required", c.TraceparentCarrierFile)
}
}
return tp
}
// PropagateTraceparent saves the traceparent to file if necessary, then prints
// span info to the console according to command-line args.
func (c Config) PropagateTraceparent(span *tracepb.Span, target io.Writer) {
var tp traceparent.Traceparent
if c.GetIsRecording() {
tp = otlpclient.TraceparentFromProtobufSpan(span, c.GetIsRecording())
} else {
// when in non-recording mode, and there is a TP available, propagate that
tp = c.LoadTraceparent()
}
if c.TraceparentCarrierFile != "" {
err := tp.SaveToFile(c.TraceparentCarrierFile, c.TraceparentPrintExport)
c.SoftFailIfErr(err)
}
if c.TraceparentPrint {
tp.Fprint(target, c.TraceparentPrintExport)
}
}
// parseHex parses hex into a []byte of length provided. Errors if the input is
// not valid hex or the converted hex is not the right number of bytes.
func parseHex(in string, expectedLen int) ([]byte, error) {
out, err := hex.DecodeString(in)
if err != nil {
return nil, fmt.Errorf("error parsing hex string %q: %w", in, err)
}
if len(out) != expectedLen {
return nil, fmt.Errorf("hex string %q is the wrong length, expected %d bytes but got %d", in, expectedLen, len(out))
}
return out, nil
}
================================================
FILE: otelcli/config_span_test.go
================================================
package otelcli
import (
"bytes"
"encoding/hex"
"fmt"
"os"
"testing"
"github.com/equinix-labs/otel-cli/otlpclient"
)
func TestPropagateTraceparent(t *testing.T) {
config := DefaultConfig().
WithTraceparentCarrierFile("").
WithTraceparentPrint(false).
WithTraceparentPrintExport(false)
tp := "00-3433d5ae39bdfee397f44be5146867b3-8a5518f1e5c54d0a-01"
tid := "3433d5ae39bdfee397f44be5146867b3"
sid := "8a5518f1e5c54d0a"
os.Setenv("TRACEPARENT", tp)
span := otlpclient.NewProtobufSpan()
span.TraceId, _ = hex.DecodeString(tid)
span.SpanId, _ = hex.DecodeString(sid)
buf := new(bytes.Buffer)
config.PropagateTraceparent(span, buf)
if buf.Len() != 0 {
t.Errorf("nothing was supposed to be written but %d bytes were", buf.Len())
}
config.TraceparentPrint = true
config.TraceparentPrintExport = true
buf = new(bytes.Buffer)
config.PropagateTraceparent(span, buf)
if buf.Len() == 0 {
t.Error("expected more than zero bytes but got none")
}
expected := fmt.Sprintf("# trace id: %s\n# span id: %s\nexport TRACEPARENT=%s\n", tid, sid, tp)
if buf.String() != expected {
t.Errorf("got unexpected output, expected '%s', got '%s'", expected, buf.String())
}
}
func TestNewProtobufSpanWithConfig(t *testing.T) {
c := DefaultConfig().WithSpanName("test span 123")
span := c.NewProtobufSpan()
if span.Name != "test span 123" {
t.Error("span event attributes must not be nil")
}
}
================================================
FILE: otelcli/config_test.go
================================================
package otelcli
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestConfig_ToStringMap(t *testing.T) {
c := Config{}
c.Headers = map[string]string{
"123test": "deadbeefcafe",
}
fsm := c.ToStringMap()
if _, ok := fsm["headers"]; !ok {
t.Errorf("missing key 'headers' in returned string map: %q", fsm)
t.Fail()
}
if fsm["headers"] != "123test=deadbeefcafe" {
t.Errorf("expected header value not found in flattened string map: %q", fsm)
t.Fail()
}
}
func TestIsRecording(t *testing.T) {
c := DefaultConfig()
if c.GetIsRecording() {
t.Fail()
}
c.Endpoint = "https://localhost:4318"
if !c.GetIsRecording() {
t.Fail()
}
}
func TestFlattenStringMap(t *testing.T) {
in := map[string]string{
"sample1": "value1",
"more": "stuff",
"getting": "bored",
"okay": "that's enough",
}
out := flattenStringMap(in, "{}")
if out != "getting=bored,more=stuff,okay=that's enough,sample1=value1" {
t.Fail()
}
}
func TestParseCkvStringMap(t *testing.T) {
expect := map[string]string{
"sample1": "value1",
"more": "stuff",
"getting": "bored",
"okay": "that's enough",
"1": "324",
}
got, err := parseCkvStringMap("1=324,getting=bored,more=stuff,okay=that's enough,sample1=value1")
if err != nil {
t.Errorf("error on valid input: %s", err)
}
if diff := cmp.Diff(expect, got); diff != "" {
t.Errorf("maps didn't match (-want +got):\n%s", diff)
}
}
func TestParseTime(t *testing.T) {
mustParse := func(layout, value string) time.Time {
out, err := time.Parse(layout, value)
if err != nil {
t.Fatalf("failed to parse time '%s' as format '%s': %s", value, layout, err)
}
return out
}
for _, testcase := range []struct {
name string
input string
want time.Time
}{
{
name: "Unix epoch time without nanoseconds",
input: "1617739561", // date +%s
want: time.Unix(1617739561, 0),
},
{
name: "Unix epoch time with nanoseconds",
input: "1617739615.759793032", // date +%s.%N
want: time.Unix(1617739615, 759793032),
},
{
name: "RFC3339",
input: "2021-04-06T13:07:54Z",
want: mustParse(time.RFC3339, "2021-04-06T13:07:54Z"),
},
{
name: "RFC3339 with nanoseconds",
input: "2021-04-06T13:12:40.792426395Z",
want: mustParse(time.RFC3339Nano, "2021-04-06T13:12:40.792426395Z"),
},
// date(1) RFC3339 format is incompatible with Go's formats
// so parseTime takes care of that automatically
{
name: "date(1) RFC3339 output, with timezone",
input: "2021-04-06 13:07:54-07:00", //date --rfc-3339=seconds
want: mustParse(time.RFC3339, "2021-04-06T13:07:54-07:00"),
},
{
name: "date(1) RFC3339 with nanoseconds and timezone",
input: "2021-04-06 13:12:40.792426395-07:00", // date --rfc-3339=ns
want: mustParse(time.RFC3339Nano, "2021-04-06T13:12:40.792426395-07:00"),
},
// TODO: maybe refactor parseTime to make failures easier to validate?
// @tobert: gonna leave that for functional tests for now
} {
t.Run(testcase.name, func(t *testing.T) {
out, _ := DefaultConfig().parseTime(testcase.input, "test")
if !out.Equal(testcase.want) {
t.Errorf("got wrong time from parseTime: %s", out.Format(time.RFC3339Nano))
}
})
}
}
func TestParseCliTime(t *testing.T) {
for _, testcase := range []struct {
name string
input string
expected time.Duration
}{
// otel-cli will still timeout but it will be the default timeouts for
// each component
{
name: "empty string returns 0 duration",
input: "",
expected: time.Duration(0),
},
{
name: "0 returns 0 duration",
input: "0",
expected: time.Duration(0),
},
{
name: "1s returns 1 second",
input: "1s",
expected: time.Second,
},
{
name: "100ms returns 100 milliseconds",
input: "100ms",
expected: time.Millisecond * 100,
},
} {
t.Run(testcase.name, func(t *testing.T) {
config := DefaultConfig().WithTimeout(testcase.input)
got := config.ParseCliTimeout()
if got != testcase.expected {
ed := testcase.expected.String()
gd := got.String()
t.Errorf("duration string %q was expected to return %s but returned %s", config.Timeout, ed, gd)
}
})
}
}
func TestParseEndpoint(t *testing.T) {
// func parseEndpoint(config Config) (*url.URL, string) {
for _, tc := range []struct {
config Config
wantEndpoint string
wantSource string
}{
// gRPC, general, bare host
{
config: DefaultConfig().WithEndpoint("localhost"),
wantEndpoint: "grpc://localhost:4317",
wantSource: "general",
},
// gRPC, general, should be bare host:port
{
config: DefaultConfig().WithEndpoint("localhost:4317"),
wantEndpoint: "grpc://localhost:4317",
wantSource: "general",
},
// gRPC, general, https URL, should transform to host:port
{
config: DefaultConfig().WithEndpoint("https://localhost:4317").WithProtocol("grpc"),
wantEndpoint: "https://localhost:4317/v1/traces",
wantSource: "general",
},
// HTTP, general, with a provided default signal path, should not be modified
{
config: DefaultConfig().WithEndpoint("http://localhost:9999/v1/traces"),
wantEndpoint: "http://localhost:9999/v1/traces",
wantSource: "general",
},
// HTTP, general, with a provided custom signal path, signal path should get appended
{
config: DefaultConfig().WithEndpoint("http://localhost:9999/my/collector/path"),
wantEndpoint: "http://localhost:9999/my/collector/path/v1/traces",
wantSource: "general",
},
// HTTPS, general, without path, should get /v1/traces appended
{
config: DefaultConfig().WithEndpoint("https://localhost:4317"),
wantEndpoint: "https://localhost:4317/v1/traces",
wantSource: "general",
},
// gRPC, signal, should come through with just the grpc:// added
{
config: DefaultConfig().WithTracesEndpoint("localhost"),
wantEndpoint: "grpc://localhost:4317",
wantSource: "signal",
},
// http, signal, should come through unmodified
{
config: DefaultConfig().WithTracesEndpoint("http://localhost"),
wantEndpoint: "http://localhost",
wantSource: "signal",
},
} {
u, src := tc.config.ParseEndpoint()
if u.String() != tc.wantEndpoint {
t.Errorf("Expected endpoint %q but got %q", tc.wantEndpoint, u.String())
}
if src != tc.wantSource {
t.Errorf("Expected source %q for test url %q but got %q", tc.wantSource, u.String(), src)
}
}
}
func TestWithEndpoint(t *testing.T) {
if DefaultConfig().WithEndpoint("foobar").Endpoint != "foobar" {
t.Fail()
}
}
func TestWithTracesEndpoint(t *testing.T) {
if DefaultConfig().WithTracesEndpoint("foobar").TracesEndpoint != "foobar" {
t.Fail()
}
}
func TestWithTimeout(t *testing.T) {
if DefaultConfig().WithTimeout("foobar").Timeout != "foobar" {
t.Fail()
}
}
func TestWithHeaders(t *testing.T) {
attr := map[string]string{"foo": "bar"}
c := DefaultConfig().WithHeaders(attr)
if diff := cmp.Diff(attr, c.Headers); diff != "" {
t.Errorf("Headers did not match (-want +got):\n%s", diff)
}
}
func TestWithInsecure(t *testing.T) {
if DefaultConfig().WithInsecure(true).Insecure != true {
t.Fail()
}
}
func TestWithBlocking(t *testing.T) {
if DefaultConfig().WithBlocking(true).Blocking != true {
t.Fail()
}
}
func TestWithTlsNoVerify(t *testing.T) {
if DefaultConfig().WithTlsNoVerify(true).TlsNoVerify != true {
t.Fail()
}
}
func TestWithTlsCACert(t *testing.T) {
if DefaultConfig().WithTlsCACert("/a/b/c").TlsCACert != "/a/b/c" {
t.Fail()
}
}
func TestWithTlsClientKey(t *testing.T) {
if DefaultConfig().WithTlsClientKey("/c/b/a").TlsClientKey != "/c/b/a" {
t.Fail()
}
}
func TestWithTlsClientCert(t *testing.T) {
if DefaultConfig().WithTlsClientCert("/b/c/a").TlsClientCert != "/b/c/a" {
t.Fail()
}
}
func TestWithServiceName(t *testing.T) {
if DefaultConfig().WithServiceName("foobar").ServiceName != "foobar" {
t.Fail()
}
}
func TestWithSpanName(t *testing.T) {
if DefaultConfig().WithSpanName("foobar").SpanName != "foobar" {
t.Fail()
}
}
func TestWithKind(t *testing.T) {
if DefaultConfig().WithKind("producer").Kind != "producer" {
t.Fail()
}
}
func TestWithAttributes(t *testing.T) {
attr := map[string]string{"foo": "bar"}
c := DefaultConfig().WithAttributes(attr)
if diff := cmp.Diff(attr, c.Attributes); diff != "" {
t.Errorf("Attributes did not match (-want +got):\n%s", diff)
}
}
func TestWithStatusCode(t *testing.T) {
if diff := cmp.Diff(DefaultConfig().WithStatusCode("unset").StatusCode, "unset"); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(DefaultConfig().WithStatusCode("ok").StatusCode, "ok"); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(DefaultConfig().WithStatusCode("error").StatusCode, "error"); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}
func TestWithStatusDescription(t *testing.T) {
if diff := cmp.Diff(DefaultConfig().WithStatusDescription("Set SCE To AUX").StatusDescription, "Set SCE To AUX"); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}
func TestWithTraceparentCarrierFile(t *testing.T) {
if DefaultConfig().WithTraceparentCarrierFile("foobar").TraceparentCarrierFile != "foobar" {
t.Fail()
}
}
func TestWithTraceparentIgnoreEnv(t *testing.T) {
if DefaultConfig().WithTraceparentIgnoreEnv(true).TraceparentIgnoreEnv != true {
t.Fail()
}
}
func TestWithTraceparentPrint(t *testing.T) {
if DefaultConfig().WithTraceparentPrint(true).TraceparentPrint != true {
t.Fail()
}
}
func TestWithTraceparentPrintExport(t *testing.T) {
if DefaultConfig().WithTraceparentPrintExport(true).TraceparentPrintExport != true {
t.Fail()
}
}
func TestWithTraceparentRequired(t *testing.T) {
if DefaultConfig().WithTraceparentRequired(true).TraceparentRequired != true {
t.Fail()
}
}
func TestWithBackgroundParentPollMs(t *testing.T) {
if DefaultConfig().WithBackgroundParentPollMs(1111).BackgroundParentPollMs != 1111 {
t.Fail()
}
}
func TestWithBackgroundSockdir(t *testing.T) {
if DefaultConfig().WithBackgroundSockdir("foobar").BackgroundSockdir != "foobar" {
t.Fail()
}
}
func TestWithBackgroundWait(t *testing.T) {
if DefaultConfig().WithBackgroundWait(true).BackgroundWait != true {
t.Fail()
}
}
func TestWithStatusCanaryCount(t *testing.T) {
if DefaultConfig().WithStatusCanaryCount(1337).StatusCanaryCount != 1337 {
t.Fail()
}
}
func TestWithStatusCanaryInterval(t *testing.T) {
if DefaultConfig().WithStatusCanaryInterval("1337ms").StatusCanaryInterval != "1337ms" {
t.Fail()
}
}
func TestWithSpanStartTime(t *testing.T) {
if DefaultConfig().WithSpanStartTime("foobar").SpanStartTime != "foobar" {
t.Fail()
}
}
func TestWithSpanEndTime(t *testing.T) {
if DefaultConfig().WithSpanEndTime("foobar").SpanEndTime != "foobar" {
t.Fail()
}
}
func TestWithEventName(t *testing.T) {
if DefaultConfig().WithEventName("foobar").EventName != "foobar" {
t.Fail()
}
}
func TestWithEventTime(t *testing.T) {
if DefaultConfig().WithEventTime("foobar").EventTime != "foobar" {
t.Fail()
}
}
func TestWithCfgFile(t *testing.T) {
if DefaultConfig().WithCfgFile("foobar").CfgFile != "foobar" {
t.Fail()
}
}
func TestWithVerbose(t *testing.T) {
if DefaultConfig().WithVerbose(true).Verbose != true {
t.Fail()
}
}
================================================
FILE: otelcli/config_tls.go
================================================
package otelcli
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/url"
"os"
)
// TlsConfig evaluates otel-cli configuration and returns a tls.Config
// that can be used by grpc or https.
func (config Config) GetTlsConfig() *tls.Config {
tlsConfig := &tls.Config{}
if config.TlsNoVerify {
Diag.InsecureSkipVerify = true
tlsConfig.InsecureSkipVerify = true
}
// puts the provided CA certificate into the root pool
// when not provided, Go TLS will automatically load the system CA pool
if config.TlsCACert != "" {
data, err := os.ReadFile(config.TlsCACert)
if err != nil {
config.SoftFail("failed to load CA certificate: %s", err)
}
certpool := x509.NewCertPool()
certpool.AppendCertsFromPEM(data)
tlsConfig.RootCAs = certpool
}
// client certificate authentication
if config.TlsClientCert != "" && config.TlsClientKey != "" {
clientPEM, err := os.ReadFile(config.TlsClientCert)
if err != nil {
config.SoftFail("failed to read client certificate file %s: %s", config.TlsClientCert, err)
}
clientKeyPEM, err := os.ReadFile(config.TlsClientKey)
if err != nil {
config.SoftFail("failed to read client key file %s: %s", config.TlsClientKey, err)
}
certPair, err := tls.X509KeyPair(clientPEM, clientKeyPEM)
if err != nil {
config.SoftFail("failed to parse client cert pair: %s", err)
}
tlsConfig.Certificates = []tls.Certificate{certPair}
} else if config.TlsClientCert != "" {
config.SoftFail("client cert and key must be specified together")
} else if config.TlsClientKey != "" {
config.SoftFail("client cert and key must be specified together")
}
return tlsConfig
}
// GetInsecure returns true if the configuration expects a non-TLS connection.
func (c Config) GetInsecure() bool {
endpointURL := c.GetEndpoint()
isLoopback, err := isLoopbackAddr(endpointURL)
c.SoftFailIfErr(err)
// Go's TLS does the right thing and forces us to say we want to disable encryption,
// but I expect most users of this program to point at a localhost endpoint that might not
// have any encryption available, or setting it up raises the bar of entry too high.
// The compromise is to automatically flip this flag to true when endpoint contains an
// an obvious "localhost", "127.0.0.x", or "::1" address.
if c.Insecure || (isLoopback && endpointURL.Scheme != "https") {
return true
} else if endpointURL.Scheme == "http" || endpointURL.Scheme == "unix" {
return true
}
return false
}
// isLoopbackAddr takes a url.URL, looks up the address, then returns true
// if it points at either a v4 or v6 loopback address.
// As I understood the OTLP spec, only host:port or an HTTP URL are acceptable.
// This function is _not_ meant to validate the endpoint, that will happen when
// otel-go attempts to connect to the endpoint.
func isLoopbackAddr(u *url.URL) (bool, error) {
hostname := u.Hostname()
if hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1" {
Diag.DetectedLocalhost = true
return true, nil
}
ips, err := net.LookupIP(hostname)
if err != nil {
return false, fmt.Errorf("unable to look up hostname '%s': %s", hostname, err)
}
// all ips returned must be loopback to return true
// cases where that isn't true should be super rare, and probably all shenanigans
allAreLoopback := true
for _, ip := range ips {
if !ip.IsLoopback() {
allAreLoopback = false
}
}
Diag.DetectedLocalhost = allAreLoopback
return allAreLoopback, nil
}
================================================
FILE: otelcli/diagnostics.go
================================================
package otelcli
import (
"strconv"
"strings"
)
// package global Diagnostics handle, written to from all over otel-cli
// and used in e.g. otel-cli status to show internal state
var Diag Diagnostics
// Diagnostics is a place to put things that are useful for testing and
// diagnosing issues with otel-cli. The only user-facing feature that should be
// using these is otel-cli status.
type Diagnostics struct {
CliArgs []string `json:"cli_args"`
IsRecording bool `json:"is_recording"`
ConfigFileLoaded bool `json:"config_file_loaded"`
NumArgs int `json:"number_of_args"`
DetectedLocalhost bool `json:"detected_localhost"`
InsecureSkipVerify bool `json:"insecure_skip_verify"`
ParsedTimeoutMs int64 `json:"parsed_timeout_ms"`
Endpoint string `json:"endpoint"` // the computed endpoint, not the raw config val
EndpointSource string `json:"endpoint_source"`
Error string `json:"error"`
ExecExitCode int `json:"exec_exit_code"`
Retries int `json:"retries"`
}
// ToMap returns the Diag struct as a string map for testing.
func (d *Diagnostics) ToStringMap() map[string]string {
return map[string]string{
"cli_args": strings.Join(d.CliArgs, " "),
"is_recording": strconv.FormatBool(d.IsRecording),
"config_file_loaded": strconv.FormatBool(d.ConfigFileLoaded),
"number_of_args": strconv.Itoa(d.NumArgs),
"detected_localhost": strconv.FormatBool(d.DetectedLocalhost),
"parsed_timeout_ms": strconv.FormatInt(d.ParsedTimeoutMs, 10),
"endpoint": d.Endpoint,
"endpoint_source": d.EndpointSource,
"error": d.Error,
}
}
// SetError sets the diagnostics Error to the error's string if it's
// not nil and returns the same error so it can be inlined in return.
func (d *Diagnostics) SetError(err error) error {
if err != nil {
Diag.Error = err.Error()
}
return err
}
// GetExitCode() is a helper for Cobra to retrieve the exit code, mainly
// used by exec to make otel-cli return the child program's exit code.
func GetExitCode() int {
return Diag.ExecExitCode
}
================================================
FILE: otelcli/exec.go
================================================
package otelcli
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"os/user"
"strings"
"time"
"github.com/equinix-labs/otel-cli/otlpclient"
"github.com/equinix-labs/otel-cli/w3c/traceparent"
"github.com/spf13/cobra"
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
tracev1 "go.opentelemetry.io/proto/otlp/trace/v1"
)
// execCmd sets up the `otel-cli exec` command
func execCmd(config *Config) *cobra.Command {
cmd := cobra.Command{
Use: "exec",
Short: "execute the command provided",
Long: `execute the command provided after the subcommand inside a span, measuring
and reporting how long it took to run. The wrapping span's w3c traceparent is automatically
passed to the child process's environment as TRACEPARENT.
Examples:
otel-cli exec -n my-cool-thing -s interesting-step curl https://cool-service/api/v1/endpoint
otel-cli exec -s "outer span" -- otel-cli exec -s "inner span" sleep 1`,
Run: doExec,
Args: cobra.MinimumNArgs(1),
}
addCommonParams(&cmd, config)
addSpanParams(&cmd, config)
addAttrParams(&cmd, config)
addClientParams(&cmd, config)
defaults := DefaultConfig()
cmd.Flags().StringVar(
&config.ExecCommandTimeout,
"command-timeout",
defaults.ExecCommandTimeout,
"timeout for the child process, when 0 otel-cli will wait forever",
)
cmd.Flags().BoolVar(
&config.ExecTpDisableInject,
"tp-disable-inject",
defaults.ExecTpDisableInject,
"disable automatically replacing {{traceparent}} with a traceparent",
)
return &cmd
}
func doExec(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
config := getConfig(ctx)
span := config.NewProtobufSpan()
processAttrs := processArgAttrs(args) // might be overwritten in process setup
// no deadline if there is no command timeout set
cancelCtxDeadline := func() {}
// fork the context for the command so its deadline doesn't impact the otlpclient ctx
cmdCtx := ctx
cmdTimeout := config.ParseExecCommandTimeout()
if cmdTimeout > 0 {
cmdCtx, cancelCtxDeadline = context.WithDeadline(ctx, time.Now().Add(cmdTimeout))
}
// pass the existing env but add the latest TRACEPARENT carrier so e.g.
// otel-cli exec 'otel-cli exec sleep 1' will relate the spans automatically
childEnv := []string{}
// set the traceparent to the current span to be available to the child process
var tp traceparent.Traceparent
if config.GetIsRecording() {
tp = otlpclient.TraceparentFromProtobufSpan(span, config.GetIsRecording())
childEnv = append(childEnv, fmt.Sprintf("TRACEPARENT=%s", tp.Encode()))
// when not recording, and a traceparent is available, pass it through
} else if !config.TraceparentIgnoreEnv {
tp := config.LoadTraceparent()
if tp.Initialized {
childEnv = append(childEnv, fmt.Sprintf("TRACEPARENT=%s", tp.Encode()))
}
}
var child *exec.Cmd
if len(args) > 1 {
tpArgs := make([]string, len(args)-1)
if config.ExecTpDisableInject {
copy(tpArgs, args[1:])
} else {
// loop over the args replacing {{traceparent}} with the current tp
for i, arg := range args[1:] {
tpArgs[i] = strings.Replace(arg, "{{traceparent}}", tp.Encode(), -1)
}
// overwrite process args attributes with the injected values
processAttrs = processArgAttrs(append([]string{args[0]}, tpArgs...))
}
child = exec.CommandContext(cmdCtx, args[0], tpArgs...)
} else {
child = exec.CommandContext(cmdCtx, args[0])
}
// attach all stdio to the parent's handles
child.Stdin = os.Stdin
child.Stdout = os.Stdout
child.Stderr = os.Stderr
// grab everything BUT the TRACEPARENT envvar
for _, env := range os.Environ() {
if !strings.HasPrefix(env, "TRACEPARENT=") {
childEnv = append(childEnv, env)
}
}
child.Env = childEnv
// ctrl-c (sigint) is forwarded to the child process
signals := make(chan os.Signal, 10)
signalsDone := make(chan struct{})
signal.Notify(signals, os.Interrupt)
go func() {
sig := <-signals
child.Process.Signal(sig)
// this might not seem necessary but without it, otel-cli exits before sending the span
close(signalsDone)
}()
span.StartTimeUnixNano = uint64(time.Now().UnixNano())
if err := child.Run(); err != nil {
span.
gitextract_c7r88qzy/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .goreleaser.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── TESTING.md
├── configs/
│ ├── otel-collector.yaml
│ └── otel-vendor-config.yaml
├── data_for_test.go
├── demos/
│ ├── 01-simple-span.sh
│ ├── 05-nested-exec.sh
│ ├── 10-span-background-simple.sh
│ ├── 15span-background-layered.sh
│ ├── 20span-background-race-workarounds.sh
│ ├── 25srecon22-talk-agenda.sh
│ └── 30-trace-build-process/
│ └── otel-wrapper-shim.sh
├── docker-compose.yaml
├── example-config.json
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── otelcli/
│ ├── completion.go
│ ├── config.go
│ ├── config_span.go
│ ├── config_span_test.go
│ ├── config_test.go
│ ├── config_tls.go
│ ├── diagnostics.go
│ ├── exec.go
│ ├── otlpclient.go
│ ├── root.go
│ ├── server.go
│ ├── server_json.go
│ ├── server_tui.go
│ ├── span.go
│ ├── span_background.go
│ ├── span_background_server.go
│ ├── span_end.go
│ ├── span_event.go
│ ├── status.go
│ ├── version.go
│ └── version_test.go
├── otlpclient/
│ ├── otlp_client.go
│ ├── otlp_client_grpc.go
│ ├── otlp_client_grpc_test.go
│ ├── otlp_client_http.go
│ ├── otlp_client_http_test.go
│ ├── otlp_client_null.go
│ ├── otlp_client_test.go
│ ├── protobuf_span.go
│ └── protobuf_span_test.go
├── otlpserver/
│ ├── grpcserver.go
│ ├── httpserver.go
│ └── server.go
├── release/
│ └── Dockerfile
├── renovate.json
├── tls_for_test.go
└── w3c/
└── traceparent/
├── traceparent.go
└── traceparent_test.go
SYMBOL INDEX (307 symbols across 39 files)
FILE: data_for_test.go
type serverProtocol (line 22) | type serverProtocol
constant grpcProtocol (line 25) | grpcProtocol serverProtocol = iota
constant httpProtocol (line 26) | httpProtocol
type CheckFunc (line 31) | type CheckFunc
type FixtureConfig (line 33) | type FixtureConfig struct
type Results (line 63) | type Results struct
type Fixture (line 86) | type Fixture struct
type FixtureSuite (line 96) | type FixtureSuite
FILE: main.go
function main (line 16) | func main() {
FILE: main_test.go
constant minimumPath (line 31) | minimumPath = `/bin:/usr/bin`
constant defaultTestTimeout (line 32) | defaultTestTimeout = time.Second
function TestMain (line 34) | func TestMain(m *testing.M) {
function TestOtelCli (line 42) | func TestOtelCli(t *testing.T) {
function runFixture (line 140) | func runFixture(t *testing.T, fixture Fixture, wait, done chan struct{}) {
function checkAll (line 152) | func checkAll(t *testing.T, fixture Fixture, results Results) {
function checkSpanCount (line 192) | func checkSpanCount(t *testing.T, fixture Fixture, results Results) {
function checkProcess (line 201) | func checkProcess(t *testing.T, fixture Fixture, results Results) bool {
function checkOutput (line 215) | func checkOutput(t *testing.T, fixture Fixture, results Results) {
function checkStatusData (line 231) | func checkStatusData(t *testing.T, fixture Fixture, results Results) {
function checkSpanData (line 275) | func checkSpanData(t *testing.T, fixture Fixture, results Results) {
function checkHeaders (line 347) | func checkHeaders(t *testing.T, fixture Fixture, results Results) {
function checkServerMeta (line 370) | func checkServerMeta(t *testing.T, fixture Fixture, results Results) {
function checkFuncs (line 381) | func checkFuncs(t *testing.T, fixture Fixture, results Results) {
function runOtelCli (line 389) | func runOtelCli(t *testing.T, fixture Fixture) (string, Results) {
function mkEnviron (line 596) | func mkEnviron(endpoint string, env map[string]string, tlsData TlsSettin...
function injectMapVars (line 614) | func injectMapVars(endpoint string, target map[string]string, tlsData Tl...
function injectVars (line 626) | func injectVars(in, endpoint string, tlsData TlsSettings) string {
FILE: otelcli/completion.go
function completionCmd (line 10) | func completionCmd(*Config) *cobra.Command {
FILE: otelcli/config.go
function init (line 23) | func init() {
function DefaultConfig (line 29) | func DefaultConfig() Config {
type Config (line 76) | type Config struct
method LoadFile (line 133) | func (c *Config) LoadFile() error {
method LoadEnv (line 155) | func (c *Config) LoadEnv(getenv func(string) string) error {
method ToStringMap (line 208) | func (c Config) ToStringMap() map[string]string {
method GetIsRecording (line 248) | func (c Config) GetIsRecording() bool {
method ParseCliTimeout (line 259) | func (c Config) ParseCliTimeout() time.Duration {
method ParseExecCommandTimeout (line 268) | func (c Config) ParseExecCommandTimeout() time.Duration {
method ParseStatusCanaryInterval (line 278) | func (c Config) ParseStatusCanaryInterval() time.Duration {
method ParseEndpoint (line 305) | func (config Config) ParseEndpoint() (*url.URL, string) {
method SoftLog (line 359) | func (c Config) SoftLog(format string, a ...interface{}) {
method SoftLogIfErr (line 369) | func (c Config) SoftLogIfErr(err error) {
method SoftFail (line 378) | func (c Config) SoftFail(format string, a ...interface{}) {
method SoftFailIfErr (line 391) | func (c Config) SoftFailIfErr(err error) {
method ParseSpanStartTime (line 448) | func (c Config) ParseSpanStartTime() time.Time {
method ParseSpanEndTime (line 455) | func (c Config) ParseSpanEndTime() time.Time {
method ParsedEventTime (line 462) | func (c Config) ParsedEventTime() time.Time {
method parseTime (line 469) | func (c Config) parseTime(ts, which string) (time.Time, error) {
method GetEndpoint (line 524) | func (c Config) GetEndpoint() *url.URL {
method WithEndpoint (line 530) | func (c Config) WithEndpoint(with string) Config {
method WithTracesEndpoint (line 536) | func (c Config) WithTracesEndpoint(with string) Config {
method WithProtocol (line 542) | func (c Config) WithProtocol(with string) Config {
method GetTimeout (line 548) | func (c Config) GetTimeout() time.Duration {
method WithTimeout (line 553) | func (c Config) WithTimeout(with string) Config {
method GetHeaders (line 559) | func (c Config) GetHeaders() map[string]string {
method WithHeaders (line 564) | func (c Config) WithHeaders(with map[string]string) Config {
method WithInsecure (line 570) | func (c Config) WithInsecure(with bool) Config {
method WithBlocking (line 576) | func (c Config) WithBlocking(with bool) Config {
method WithTlsNoVerify (line 582) | func (c Config) WithTlsNoVerify(with bool) Config {
method WithTlsCACert (line 588) | func (c Config) WithTlsCACert(with string) Config {
method WithTlsClientKey (line 594) | func (c Config) WithTlsClientKey(with string) Config {
method WithTlsClientCert (line 600) | func (c Config) WithTlsClientCert(with string) Config {
method GetServiceName (line 606) | func (c Config) GetServiceName() string {
method WithServiceName (line 611) | func (c Config) WithServiceName(with string) Config {
method WithSpanName (line 617) | func (c Config) WithSpanName(with string) Config {
method WithKind (line 623) | func (c Config) WithKind(with string) Config {
method WithAttributes (line 629) | func (c Config) WithAttributes(with map[string]string) Config {
method WithStatusCode (line 635) | func (c Config) WithStatusCode(with string) Config {
method WithStatusDescription (line 641) | func (c Config) WithStatusDescription(with string) Config {
method WithTraceparentCarrierFile (line 647) | func (c Config) WithTraceparentCarrierFile(with string) Config {
method WithTraceparentIgnoreEnv (line 653) | func (c Config) WithTraceparentIgnoreEnv(with bool) Config {
method WithTraceparentPrint (line 659) | func (c Config) WithTraceparentPrint(with bool) Config {
method WithTraceparentPrintExport (line 665) | func (c Config) WithTraceparentPrintExport(with bool) Config {
method WithTraceparentRequired (line 671) | func (c Config) WithTraceparentRequired(with bool) Config {
method WithBackgroundParentPollMs (line 677) | func (c Config) WithBackgroundParentPollMs(with int) Config {
method WithBackgroundSockdir (line 683) | func (c Config) WithBackgroundSockdir(with string) Config {
method WithBackgroundWait (line 689) | func (c Config) WithBackgroundWait(with bool) Config {
method WithBackgroundSkipParentPidCheck (line 695) | func (c Config) WithBackgroundSkipParentPidCheck(with bool) Config {
method WithStatusCanaryCount (line 701) | func (c Config) WithStatusCanaryCount(with int) Config {
method WithStatusCanaryInterval (line 707) | func (c Config) WithStatusCanaryInterval(with string) Config {
method WithSpanStartTime (line 713) | func (c Config) WithSpanStartTime(with string) Config {
method WithSpanEndTime (line 719) | func (c Config) WithSpanEndTime(with string) Config {
method WithEventName (line 725) | func (c Config) WithEventName(with string) Config {
method WithEventTime (line 731) | func (c Config) WithEventTime(with string) Config {
method WithCfgFile (line 737) | func (c Config) WithCfgFile(with string) Config {
method WithVerbose (line 743) | func (c Config) WithVerbose(with bool) Config {
method WithFail (line 749) | func (c Config) WithFail(with bool) Config {
method GetVersion (line 755) | func (c Config) GetVersion() string {
method WithVersion (line 760) | func (c Config) WithVersion(with string) Config {
function parseDuration (line 287) | func parseDuration(d string) (time.Duration, error) {
function flattenStringMap (line 400) | func flattenStringMap(mp map[string]string, emptyValue string) string {
function parseCkvStringMap (line 427) | func parseCkvStringMap(in string) (map[string]string, error) {
FILE: otelcli/config_span.go
method NewProtobufSpan (line 16) | func (c Config) NewProtobufSpan() *tracepb.Span {
method LoadTraceparent (line 77) | func (c Config) LoadTraceparent() traceparent.Traceparent {
method PropagateTraceparent (line 116) | func (c Config) PropagateTraceparent(span *tracepb.Span, target io.Write...
function parseHex (line 137) | func parseHex(in string, expectedLen int) ([]byte, error) {
FILE: otelcli/config_span_test.go
function TestPropagateTraceparent (line 13) | func TestPropagateTraceparent(t *testing.T) {
function TestNewProtobufSpanWithConfig (line 47) | func TestNewProtobufSpanWithConfig(t *testing.T) {
FILE: otelcli/config_test.go
function TestConfig_ToStringMap (line 10) | func TestConfig_ToStringMap(t *testing.T) {
function TestIsRecording (line 29) | func TestIsRecording(t *testing.T) {
function TestFlattenStringMap (line 41) | func TestFlattenStringMap(t *testing.T) {
function TestParseCkvStringMap (line 56) | func TestParseCkvStringMap(t *testing.T) {
function TestParseTime (line 75) | func TestParseTime(t *testing.T) {
function TestParseCliTime (line 133) | func TestParseCliTime(t *testing.T) {
function TestParseEndpoint (line 174) | func TestParseEndpoint(t *testing.T) {
function TestWithEndpoint (line 243) | func TestWithEndpoint(t *testing.T) {
function TestWithTracesEndpoint (line 248) | func TestWithTracesEndpoint(t *testing.T) {
function TestWithTimeout (line 253) | func TestWithTimeout(t *testing.T) {
function TestWithHeaders (line 258) | func TestWithHeaders(t *testing.T) {
function TestWithInsecure (line 265) | func TestWithInsecure(t *testing.T) {
function TestWithBlocking (line 270) | func TestWithBlocking(t *testing.T) {
function TestWithTlsNoVerify (line 275) | func TestWithTlsNoVerify(t *testing.T) {
function TestWithTlsCACert (line 280) | func TestWithTlsCACert(t *testing.T) {
function TestWithTlsClientKey (line 285) | func TestWithTlsClientKey(t *testing.T) {
function TestWithTlsClientCert (line 290) | func TestWithTlsClientCert(t *testing.T) {
function TestWithServiceName (line 295) | func TestWithServiceName(t *testing.T) {
function TestWithSpanName (line 300) | func TestWithSpanName(t *testing.T) {
function TestWithKind (line 305) | func TestWithKind(t *testing.T) {
function TestWithAttributes (line 310) | func TestWithAttributes(t *testing.T) {
function TestWithStatusCode (line 318) | func TestWithStatusCode(t *testing.T) {
function TestWithStatusDescription (line 332) | func TestWithStatusDescription(t *testing.T) {
function TestWithTraceparentCarrierFile (line 338) | func TestWithTraceparentCarrierFile(t *testing.T) {
function TestWithTraceparentIgnoreEnv (line 343) | func TestWithTraceparentIgnoreEnv(t *testing.T) {
function TestWithTraceparentPrint (line 348) | func TestWithTraceparentPrint(t *testing.T) {
function TestWithTraceparentPrintExport (line 353) | func TestWithTraceparentPrintExport(t *testing.T) {
function TestWithTraceparentRequired (line 358) | func TestWithTraceparentRequired(t *testing.T) {
function TestWithBackgroundParentPollMs (line 363) | func TestWithBackgroundParentPollMs(t *testing.T) {
function TestWithBackgroundSockdir (line 368) | func TestWithBackgroundSockdir(t *testing.T) {
function TestWithBackgroundWait (line 373) | func TestWithBackgroundWait(t *testing.T) {
function TestWithStatusCanaryCount (line 378) | func TestWithStatusCanaryCount(t *testing.T) {
function TestWithStatusCanaryInterval (line 383) | func TestWithStatusCanaryInterval(t *testing.T) {
function TestWithSpanStartTime (line 388) | func TestWithSpanStartTime(t *testing.T) {
function TestWithSpanEndTime (line 393) | func TestWithSpanEndTime(t *testing.T) {
function TestWithEventName (line 398) | func TestWithEventName(t *testing.T) {
function TestWithEventTime (line 403) | func TestWithEventTime(t *testing.T) {
function TestWithCfgFile (line 408) | func TestWithCfgFile(t *testing.T) {
function TestWithVerbose (line 413) | func TestWithVerbose(t *testing.T) {
FILE: otelcli/config_tls.go
method GetTlsConfig (line 14) | func (config Config) GetTlsConfig() *tls.Config {
method GetInsecure (line 60) | func (c Config) GetInsecure() bool {
function isLoopbackAddr (line 85) | func isLoopbackAddr(u *url.URL) (bool, error) {
FILE: otelcli/diagnostics.go
type Diagnostics (line 15) | type Diagnostics struct
method ToStringMap (line 31) | func (d *Diagnostics) ToStringMap() map[string]string {
method SetError (line 47) | func (d *Diagnostics) SetError(err error) error {
function GetExitCode (line 56) | func GetExitCode() int {
FILE: otelcli/exec.go
function execCmd (line 21) | func execCmd(config *Config) *cobra.Command {
function doExec (line 61) | func doExec(cmd *cobra.Command, args []string) {
function processArgAttrs (line 180) | func processArgAttrs(args []string) []*commonpb.KeyValue {
function processPidAttrs (line 211) | func processPidAttrs(config Config, ppid, pid int64) []*commonpb.KeyValue {
FILE: otelcli/otlpclient.go
function StartClient (line 13) | func StartClient(ctx context.Context, config Config) (context.Context, o...
FILE: otelcli/root.go
type cliContextKey (line 13) | type cliContextKey
function configContextKey (line 16) | func configContextKey() cliContextKey {
function getConfigRef (line 22) | func getConfigRef(ctx context.Context) *Config {
function getConfig (line 35) | func getConfig(ctx context.Context) Config {
function createRootCmd (line 42) | func createRootCmd(config *Config) *cobra.Command {
function Execute (line 82) | func Execute(version string) {
function addCommonParams (line 94) | func addCommonParams(cmd *cobra.Command, config *Config) {
function addClientParams (line 117) | func addClientParams(cmd *cobra.Command, config *Config) {
function addSpanParams (line 144) | func addSpanParams(cmd *cobra.Command, config *Config) {
function addSpanStartEndParams (line 162) | func addSpanStartEndParams(cmd *cobra.Command, config *Config) {
function addSpanStatusParams (line 172) | func addSpanStatusParams(cmd *cobra.Command, config *Config) {
function addAttrParams (line 181) | func addAttrParams(cmd *cobra.Command, config *Config) {
FILE: otelcli/server.go
constant defaultOtlpEndpoint (line 10) | defaultOtlpEndpoint = "grpc://localhost:4317"
constant spanBgSockfilename (line 11) | spanBgSockfilename = "otel-cli-background.sock"
function serverCmd (line 13) | func serverCmd(config *Config) *cobra.Command {
function runServer (line 28) | func runServer(config Config, cb otlpserver.Callback, stop otlpserver.St...
FILE: otelcli/server_json.go
function serverJsonCmd (line 26) | func serverJsonCmd(config *Config) *cobra.Command {
function doServerJson (line 42) | func doServerJson(cmd *cobra.Command, args []string) {
function renderJson (line 61) | func renderJson(ctx context.Context, span *tracepb.Span, events []*trace...
function writeJson (line 110) | func writeJson(path, filename string, js []byte) {
FILE: otelcli/server_tui.go
function serverTuiCmd (line 24) | func serverTuiCmd(config *Config) *cobra.Command {
function doServerTui (line 40) | func doServerTui(cmd *cobra.Command, args []string) {
function renderTui (line 60) | func renderTui(ctx context.Context, span *tracepb.Span, events []*tracep...
function roundedDelta (line 132) | func roundedDelta(ts1, ts2 uint64) int64 {
function trimTuiEvents (line 141) | func trimTuiEvents() {
type SpanEventUnion (line 171) | type SpanEventUnion struct
method TraceIdString (line 176) | func (seu *SpanEventUnion) TraceIdString() string { return hex.EncodeT...
method SpanIdString (line 177) | func (seu *SpanEventUnion) SpanIdString() string { return hex.EncodeT...
method UnixNanos (line 179) | func (seu *SpanEventUnion) UnixNanos() uint64 {
method IsSpan (line 189) | func (seu *SpanEventUnion) IsSpan() bool { return seu.Event == nil }
type SpanEventUnionList (line 192) | type SpanEventUnionList
method Len (line 194) | func (sl SpanEventUnionList) Len() int { return len(sl) }
method Swap (line 195) | func (sl SpanEventUnionList) Swap(i, j int) { sl[i], sl[j] = sl[j...
method Less (line 196) | func (sl SpanEventUnionList) Less(i, j int) bool { return sl[i].UnixNa...
FILE: otelcli/span.go
function spanCmd (line 13) | func spanCmd(config *Config) *cobra.Command {
function doSpan (line 49) | func doSpan(cmd *cobra.Command, args []string) {
FILE: otelcli/span_background.go
function spanBgCmd (line 18) | func spanBgCmd(config *Config) *cobra.Command {
function doSpanBackground (line 63) | func doSpanBackground(cmd *cobra.Command, args []string) {
function spanBgEndEvent (line 148) | func spanBgEndEvent(ctx context.Context, span *tracepb.Span, name string...
FILE: otelcli/span_background_server.go
type BgSpan (line 20) | type BgSpan struct
method AddEvent (line 45) | func (bs BgSpan) AddEvent(bse *BgSpanEvent, reply *BgSpan) error {
method Wait (line 67) | func (bs BgSpan) Wait(in, reply *struct{}) error {
method End (line 73) | func (bs BgSpan) End(in *BgEnd, reply *BgSpan) error {
type BgSpanEvent (line 31) | type BgSpanEvent struct
type BgEnd (line 38) | type BgEnd struct
type bgServer (line 94) | type bgServer struct
method Run (line 140) | func (bgs *bgServer) Run() {
method Shutdown (line 164) | func (bgs *bgServer) Shutdown() {
function createBgServer (line 104) | func createBgServer(ctx context.Context, sockfile string, span *tracepb....
function createBgClient (line 174) | func createBgClient(config Config) (*rpc.Client, func()) {
FILE: otelcli/span_end.go
function spanEndCmd (line 11) | func spanEndCmd(config *Config) *cobra.Command {
function doSpanEnd (line 41) | func doSpanEnd(cmd *cobra.Command, args []string) {
FILE: otelcli/span_event.go
function spanEventCmd (line 12) | func spanEventCmd(config *Config) *cobra.Command {
function doSpanEvent (line 48) | func doSpanEvent(cmd *cobra.Command, args []string) {
FILE: otelcli/status.go
type StatusOutput (line 20) | type StatusOutput struct
function statusCmd (line 29) | func statusCmd(config *Config) *cobra.Command {
function doStatus (line 57) | func doStatus(cmd *cobra.Command, args []string) {
FILE: otelcli/version.go
function versionCmd (line 12) | func versionCmd(_ *Config) *cobra.Command {
function doVersion (line 22) | func doVersion(cmd *cobra.Command, args []string) {
function FormatVersion (line 30) | func FormatVersion(version, commit, date string) string {
FILE: otelcli/version_test.go
function TestFormatVersion (line 9) | func TestFormatVersion(t *testing.T) {
FILE: otlpclient/otlp_client.go
type OTLPClient (line 22) | type OTLPClient interface
type OTLPConfig (line 29) | type OTLPConfig interface
function SendSpan (line 41) | func SendSpan(ctx context.Context, client OTLPClient, config OTLPConfig,...
function resourceAttributes (line 80) | func resourceAttributes(ctx context.Context, serviceName string) ([]*com...
type otlpClientCtxKey (line 127) | type otlpClientCtxKey
type TimestampedError (line 130) | type TimestampedError struct
type ErrorList (line 136) | type ErrorList
function errorListKey (line 139) | func errorListKey() otlpClientCtxKey {
function GetErrorList (line 145) | func GetErrorList(ctx context.Context) ErrorList {
function SaveError (line 159) | func SaveError(ctx context.Context, t time.Time, err error) (context.Con...
function retry (line 193) | func retry(ctx context.Context, _ OTLPConfig, fun retryFun) (context.Con...
type retryFun (line 233) | type retryFun
FILE: otlpclient/otlp_client_grpc.go
type GrpcClient (line 20) | type GrpcClient struct
method Start (line 33) | func (gc *GrpcClient) Start(ctx context.Context) (context.Context, err...
method UploadTraces (line 62) | func (gc *GrpcClient) UploadTraces(ctx context.Context, rsps []*tracep...
method Stop (line 79) | func (gc *GrpcClient) Stop(ctx context.Context) (context.Context, erro...
function NewGrpcClient (line 27) | func NewGrpcClient(config OTLPConfig) *GrpcClient {
function processGrpcStatus (line 83) | func processGrpcStatus(ctx context.Context, _ *coltracepb.ExportTraceSer...
FILE: otlpclient/otlp_client_grpc_test.go
function TestProcessGrpcStatus (line 16) | func TestProcessGrpcStatus(t *testing.T) {
function retryWithInfo (line 83) | func retryWithInfo(wait int64) error {
FILE: otlpclient/otlp_client_http.go
type HttpClient (line 21) | type HttpClient struct
method Start (line 34) | func (hc *HttpClient) Start(ctx context.Context) (context.Context, err...
method UploadTraces (line 51) | func (hc *HttpClient) UploadTraces(ctx context.Context, rsps []*tracep...
method Stop (line 139) | func (hc *HttpClient) Stop(ctx context.Context) (context.Context, erro...
function NewHttpClient (line 27) | func NewHttpClient(config OTLPConfig) *HttpClient {
function processHTTPStatus (line 90) | func processHTTPStatus(ctx context.Context, resp *http.Response, body []...
FILE: otlpclient/otlp_client_http_test.go
function TestProcessHTTPStatus (line 16) | func TestProcessHTTPStatus(t *testing.T) {
function etsrSuccessBody (line 151) | func etsrSuccessBody() []byte {
function etsrPartialSuccessBody (line 159) | func etsrPartialSuccessBody() []byte {
function errorBody (line 170) | func errorBody(c int32, message string) []byte {
FILE: otlpclient/otlp_client_null.go
type NullClient (line 11) | type NullClient struct
method Start (line 19) | func (nc *NullClient) Start(ctx context.Context) (context.Context, err...
method UploadTraces (line 24) | func (nc *NullClient) UploadTraces(ctx context.Context, rsps []*tracep...
method Stop (line 29) | func (gc *NullClient) Stop(ctx context.Context) (context.Context, erro...
function NewNullClient (line 14) | func NewNullClient(config OTLPConfig) *NullClient {
FILE: otlpclient/otlp_client_test.go
function TestErrorLists (line 12) | func TestErrorLists(t *testing.T) {
FILE: otlpclient/protobuf_span.go
type SpanConfig (line 22) | type SpanConfig interface
function NewProtobufSpan (line 26) | func NewProtobufSpan() *tracepb.Span {
function NewProtobufSpanEvent (line 54) | func NewProtobufSpanEvent() *tracepb.Span_Event {
function SetSpanStatus (line 66) | func SetSpanStatus(span *tracepb.Span, status string, message string) {
function GetEmptyTraceId (line 75) | func GetEmptyTraceId() []byte {
function GetEmptySpanId (line 80) | func GetEmptySpanId() []byte {
function GenerateTraceId (line 85) | func GenerateTraceId() []byte {
function GenerateSpanId (line 96) | func GenerateSpanId() []byte {
function SpanKindIntToString (line 108) | func SpanKindIntToString(kind tracepb.Span_SpanKind) string {
function SpanKindStringToInt (line 127) | func SpanKindStringToInt(kind string) tracepb.Span_SpanKind {
function SpanStatusStringToInt (line 146) | func SpanStatusStringToInt(status string) tracepb.Status_StatusCode {
function StringMapAttrsToProtobuf (line 161) | func StringMapAttrsToProtobuf(attributes map[string]string) []*commonpb....
function SpanAttributesToStringMap (line 190) | func SpanAttributesToStringMap(span *tracepb.Span) map[string]string {
function ResourceAttributesToStringMap (line 200) | func ResourceAttributesToStringMap(rss *tracepb.ResourceSpans) map[strin...
function AnyValueToString (line 213) | func AnyValueToString(v *commonpb.AnyValue) string {
function SpanToStringMap (line 235) | func SpanToStringMap(span *tracepb.Span, rss *tracepb.ResourceSpans) map...
function TraceparentFromProtobufSpan (line 255) | func TraceparentFromProtobufSpan(span *tracepb.Span, recording bool) tra...
function flattenStringMap (line 268) | func flattenStringMap(mp map[string]string, emptyValue string) string {
FILE: otlpclient/protobuf_span_test.go
function TestNewProtobufSpan (line 11) | func TestNewProtobufSpan(t *testing.T) {
function TestNewProtobufSpanEvent (line 36) | func TestNewProtobufSpanEvent(t *testing.T) {
function TestGenerateTraceId (line 46) | func TestGenerateTraceId(t *testing.T) {
function TestGenerateSpanId (line 58) | func TestGenerateSpanId(t *testing.T) {
function TestSpanKindStringToInt (line 70) | func TestSpanKindStringToInt(t *testing.T) {
function TestSpanKindIntToString (line 113) | func TestSpanKindIntToString(t *testing.T) {
function TestSpanStatusStringToInt (line 153) | func TestSpanStatusStringToInt(t *testing.T) {
function TestCliAttrsToOtel (line 185) | func TestCliAttrsToOtel(t *testing.T) {
FILE: otlpserver/grpcserver.go
type GrpcServer (line 18) | type GrpcServer struct
method Serve (line 52) | func (gs *GrpcServer) Serve(listener net.Listener) error {
method ListenAndServe (line 60) | func (gs *GrpcServer) ListenAndServe(otlpEndpoint string) {
method Stop (line 72) | func (gs *GrpcServer) Stop() {
method StopWait (line 79) | func (gs *GrpcServer) StopWait() {
method Export (line 87) | func (gs *GrpcServer) Export(ctx context.Context, req *coltracepb.Expo...
function NewGrpcServer (line 30) | func NewGrpcServer(cb Callback, stop Stopper) *GrpcServer {
FILE: otlpserver/httpserver.go
type HttpServer (line 16) | type HttpServer struct
method ServeHTTP (line 36) | func (hs *HttpServer) ServeHTTP(rw http.ResponseWriter, req *http.Requ...
method Serve (line 73) | func (hs *HttpServer) Serve(listener net.Listener) error {
method ListenAndServe (line 80) | func (hs *HttpServer) ListenAndServe(otlpEndpoint string) {
method Stop (line 91) | func (hs *HttpServer) Stop() {
method StopWait (line 96) | func (hs *HttpServer) StopWait() {
function NewHttpServer (line 23) | func NewHttpServer(cb Callback, stop Stopper) *HttpServer {
FILE: otlpserver/server.go
type Callback (line 17) | type Callback
type Stopper (line 21) | type Stopper
type OtlpServer (line 25) | type OtlpServer interface
function NewServer (line 34) | func NewServer(protocol string, cb Callback, stop Stopper) OtlpServer {
function doCallback (line 47) | func doCallback(ctx context.Context, cb Callback, req *colv1.ExportTrace...
FILE: tls_for_test.go
type TlsSettings (line 27) | type TlsSettings struct
method cleanup (line 164) | func (t TlsSettings) cleanup() {
function generateTLSData (line 39) | func generateTLSData(t *testing.T) TlsSettings {
function pemToTempFile (line 173) | func pemToTempFile(t *testing.T, tmpl string, buf *bytes.Buffer) string {
FILE: w3c/traceparent/traceparent.go
function init (line 20) | func init() {
type Traceparent (line 27) | type Traceparent struct
method Encode (line 36) | func (tp Traceparent) Encode() string {
method TraceIdString (line 59) | func (tp Traceparent) TraceIdString() string {
method SpanIdString (line 68) | func (tp Traceparent) SpanIdString() string {
method SaveToFile (line 122) | func (tp Traceparent) SaveToFile(carrierFile string, export bool) error {
method Fprint (line 135) | func (tp Traceparent) Fprint(target io.Writer, export bool) error {
function LoadFromFile (line 80) | func LoadFromFile(filename string) (Traceparent, error) {
function LoadFromEnv (line 151) | func LoadFromEnv() (Traceparent, error) {
function Parse (line 161) | func Parse(tp string) (Traceparent, error) {
FILE: w3c/traceparent/traceparent_test.go
function TestFprint (line 12) | func TestFprint(t *testing.T) {
function TestLoadTraceparent (line 74) | func TestLoadTraceparent(t *testing.T) {
function TestWriteTraceparentToFile (line 120) | func TestWriteTraceparentToFile(t *testing.T) {
Condensed preview — 62 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (336K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 779,
"preview": "name: CI\non:\n push:\n branches: [ main ]\n pull_request:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n -"
},
{
"path": ".gitignore",
"chars": 292,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\notel-cli\n\n# Test binary, built with `go test -c`\n*.t"
},
{
"path": ".goreleaser.yml",
"chars": 3446,
"preview": "# last updated for goreleaser v1.14.1\n\nbefore:\n hooks:\n - go mod tidy\n\nchecksum:\n name_template: 'checksums.txt'\n\ns"
},
{
"path": "CHANGELOG.md",
"chars": 7785,
"preview": "## [0.4.6] - 2024-05-13\n\nBuild smaller binaries and add version subcommand.\n\n### Added\n\n- `otel-cli version` will now re"
},
{
"path": "Dockerfile",
"chars": 217,
"preview": "FROM golang:latest AS builder\n\nWORKDIR /build\nCOPY . .\nENV CGO_ENABLED=0\nRUN go build -ldflags=\"-w -s\" -o otel-cli .\n\nFR"
},
{
"path": "LICENSE",
"chars": 11348,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 14047,
"preview": "# otel-cli\n\n[](https://github.com/packethost/stand"
},
{
"path": "TESTING.md",
"chars": 3893,
"preview": "# Testing otel-cli\n\n## Synopsis\n\notel-cli's primary method of testing is functional, implemented in\n`main_test.go` and a"
},
{
"path": "configs/otel-collector.yaml",
"chars": 304,
"preview": "---\nreceivers:\n otlp:\n protocols:\n grpc:\n\nexporters:\n logging:\n loglevel: debug\n jaeger:\n endpoint: \"ja"
},
{
"path": "configs/otel-vendor-config.yaml",
"chars": 1603,
"preview": "# opentelemetry-collector is a proxy for telemetry events.\n#\n# This configuration is set up for use in otel-cli developm"
},
{
"path": "data_for_test.go",
"chars": 42612,
"preview": "package main_test\n\n// This file implements data structures and data for functional testing of\n// otel-cli.\n//\n// See: TE"
},
{
"path": "demos/01-simple-span.sh",
"chars": 518,
"preview": "#!/bin/bash\n# an otel-cli demo\n\n# this isn't super precise because of process timing but good enough\n# in many cases to "
},
{
"path": "demos/05-nested-exec.sh",
"chars": 901,
"preview": "#!/bin/bash\n# an otel-cli demo of nested exec\n#\n# this isn't necessarily practical, but it demonstrates how the TRACEPAR"
},
{
"path": "demos/10-span-background-simple.sh",
"chars": 340,
"preview": "#!/bin/bash\n# an otel-cli demo of span background\n\n../otel-cli span background \\\n --service otel-cli-demo \\\n --nam"
},
{
"path": "demos/15span-background-layered.sh",
"chars": 1529,
"preview": "#!/bin/bash\n# an otel-cli demo of span background\n#\n# This demo shows span background functionality with events added to"
},
{
"path": "demos/20span-background-race-workarounds.sh",
"chars": 1874,
"preview": "#!/bin/bash\n# an otel-cli demo of workarounds for race conditions on span background\n#\n# otel-cli span background is usu"
},
{
"path": "demos/25srecon22-talk-agenda.sh",
"chars": 3992,
"preview": "#!/bin/bash\n\n# a quick script to render a talk agenda using OTel\n\nsvc=\"SRECon\"\n\n# turns out the date doesn't matter much"
},
{
"path": "demos/30-trace-build-process/otel-wrapper-shim.sh",
"chars": 2115,
"preview": "#!/bin/bash\n\n# Build Process Tracing Example\n#\n# It's possible to instrument complex build processes in nodejs (npm/yarn"
},
{
"path": "docker-compose.yaml",
"chars": 371,
"preview": "---\nversion: '2.1'\nservices:\n jaeger:\n image: jaegertracing/all-in-one:1.58.0\n ports:\n - \"16686:16686\"\n "
},
{
"path": "example-config.json",
"chars": 1048,
"preview": "{\n \"endpoint\" : \"localhost:4317\",\n \"traces_endpoint\": \"\",\n \"timeout\" : \"1s\",\n \"otlp_headers\" : {\n \"header1\""
},
{
"path": "go.mod",
"chars": 1532,
"preview": "module github.com/equinix-labs/otel-cli\n\ngo 1.21\n\ntoolchain go1.22.4\n\nrequire (\n\tgithub.com/google/go-cmp v0.6.0\n\tgithub"
},
{
"path": "go.sum",
"chars": 17463,
"preview": "atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=\natomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3Q"
},
{
"path": "main.go",
"chars": 298,
"preview": "package main\n\nimport (\n\t\"os\"\n\n\t\"github.com/equinix-labs/otel-cli/otelcli\"\n)\n\n// these will be set by goreleaser & ldflag"
},
{
"path": "main_test.go",
"chars": 22595,
"preview": "package main_test\n\n// This file implements the data-driven test harness for otel-cli. It executes\n// tests defined in da"
},
{
"path": "otelcli/completion.go",
"chars": 2203,
"preview": "package otelcli\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc completionCmd(*Config) *cobra.Command {\n\tcmd :"
},
{
"path": "otelcli/config.go",
"chars": 26095,
"preview": "package otelcli\n\nimport (\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"reflect\"\n\t"
},
{
"path": "otelcli/config_span.go",
"chars": 4201,
"preview": "package otelcli\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpclient\"\n\t\"github."
},
{
"path": "otelcli/config_span_test.go",
"chars": 1416,
"preview": "package otelcli\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpclien"
},
{
"path": "otelcli/config_test.go",
"chars": 11354,
"preview": "package otelcli\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestConfig_ToStringMap(t *testing."
},
{
"path": "otelcli/config_tls.go",
"chars": 3463,
"preview": "package otelcli\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n)\n\n// TlsConfig evaluates otel-cli"
},
{
"path": "otelcli/diagnostics.go",
"chars": 2160,
"preview": "package otelcli\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// package global Diagnostics handle, written to from all over otel-c"
},
{
"path": "otelcli/exec.go",
"chars": 6823,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"os/user\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/eq"
},
{
"path": "otelcli/otlpclient.go",
"chars": 1125,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpclient\"\n)\n\n// StartClient"
},
{
"path": "otelcli/root.go",
"chars": 9570,
"preview": "// Package otelcli implements the otel-cli subcommands and argument parsing\n// with Cobra and implements functionality u"
},
{
"path": "otelcli/server.go",
"chars": 1342,
"preview": "package otelcli\n\nimport (\n\t\"strings\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpserver\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst "
},
{
"path": "otelcli/server_json.go",
"chars": 3516,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n"
},
{
"path": "otelcli/server_tui.go",
"chars": 6239,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"log\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\n\t\"github.com/equinix-labs/otel-c"
},
{
"path": "otelcli/span.go",
"chars": 1515,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpclient\"\n\t\"github.com/spf13/co"
},
{
"path": "otelcli/span_background.go",
"chars": 5218,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path\"\n\t\"strconv\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/equinix-labs"
},
{
"path": "otelcli/span_background_server.go",
"chars": 5925,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/rpc\"\n\t\"net/rpc/jsonrpc\"\n\t\"os\"\n\t\"path\"\n\t\"sync\"\n\t"
},
{
"path": "otelcli/span_end.go",
"chars": 1841,
"preview": "package otelcli\n\nimport (\n\t\"os\"\n\n\t\"github.com/equinix-labs/otel-cli/w3c/traceparent\"\n\t\"github.com/spf13/cobra\"\n)\n\n// spa"
},
{
"path": "otelcli/span_event.go",
"chars": 2202,
"preview": "package otelcli\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/w3c/traceparent\"\n\t\"github.com/spf13/cobra\"\n)"
},
{
"path": "otelcli/status.go",
"chars": 5033,
"preview": "package otelcli\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gith"
},
{
"path": "otelcli/version.go",
"chars": 956,
"preview": "package otelcli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// versionCmd prints the version and exi"
},
{
"path": "otelcli/version_test.go",
"chars": 753,
"preview": "package otelcli\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestFormatVersion(t *testing.T) {\n\temptyVa"
},
{
"path": "otlpclient/otlp_client.go",
"chars": 7403,
"preview": "// Package otlpclient implements a simple OTLP client, directly working with\n// protobuf, gRPC, and net/http with minima"
},
{
"path": "otlpclient/otlp_client_grpc.go",
"chars": 3666,
"preview": "package otlpclient\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\tcoltracepb \"go.opentelemetry.io/proto/otlp/collector/trace/v1\"\n"
},
{
"path": "otlpclient/otlp_client_grpc_test.go",
"chars": 2544,
"preview": "package otlpclient\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\tcoltracepb \"go.opentelemetr"
},
{
"path": "otlpclient/otlp_client_http.go",
"chars": 5105,
"preview": "package otlpclient\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\tcolt"
},
{
"path": "otlpclient/otlp_client_http_test.go",
"chars": 4451,
"preview": "package otlpclient\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\tcoltracepb \"go.o"
},
{
"path": "otlpclient/otlp_client_null.go",
"chars": 850,
"preview": "package otlpclient\n\nimport (\n\t\"context\"\n\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// NullClient is an OTLP "
},
{
"path": "otlpclient/otlp_client_test.go",
"chars": 788,
"preview": "package otlpclient\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestErrorList"
},
{
"path": "otlpclient/protobuf_span.go",
"chars": 8962,
"preview": "package otlpclient\n\n// Implements just enough sugar on the OTel Protocol Buffers span definition\n// to support otel-cli "
},
{
"path": "otlpclient/protobuf_span_test.go",
"chars": 5585,
"preview": "package otlpclient\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\t\"testing\"\n\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\nfunc "
},
{
"path": "otlpserver/grpcserver.go",
"chars": 2805,
"preview": "package otlpserver\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/csv\"\n\t\"log\"\n\t\"net\"\n\t\"sync\"\n\n\tcoltracepb \"go.opentelemetry.io"
},
{
"path": "otlpserver/httpserver.go",
"chars": 2416,
"preview": "package otlpserver\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\n\tcoltracepb \"go.opentelemetry."
},
{
"path": "otlpserver/server.go",
"chars": 2019,
"preview": "// otlpserver is an OTLP server with HTTP and gRPC backends available.\n// It takes a lot of shortcuts to keep things sim"
},
{
"path": "release/Dockerfile",
"chars": 363,
"preview": "# While the top-level Dockerfile is set up for local development on otel-cli,\n# this Dockerfile is only for release.\n#\n#"
},
{
"path": "renovate.json",
"chars": 107,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:base\"\n ]\n}\n"
},
{
"path": "tls_for_test.go",
"chars": 5159,
"preview": "package main_test\n\n/*\n * This file implements a certificate authority and certs for testing otel-cli's\n * TLS settings.\n"
},
{
"path": "w3c/traceparent/traceparent.go",
"chars": 5629,
"preview": "// Package traceparent contains a lightweight implementation of W3C\n// traceparent parsing, loading from files and envir"
},
{
"path": "w3c/traceparent/traceparent_test.go",
"chars": 4770,
"preview": "package traceparent\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestFprint(t"
}
]
About this extraction
This page contains the full source code of the equinix-labs/otel-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 62 files (299.3 KB), approximately 90.0k tokens, and a symbol index with 307 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.