[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non:\n  push:\n    branches: [ main ]\n  pull_request:\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4\n      - name: Setup\n        uses: actions/setup-go@v5\n        with:\n          go-version-file: go.mod\n      - name: Get dependencies\n        run: go mod download\n      # otel-cli's main test needs the binary built ahead of time\n      # also this validates it can acutally build before we get there\n      - name: Build\n        # build with -s -w to reduce binary size and verify that build in test\n        run: go build -v -ldflags=\"-s -w -X main.version=test -X main.commit=${{ github.sha }}\"\n      - name: Test\n        run: go test -v -cover -parallel 4 ./...\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\ndist/\n\n*.pem\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# last updated for goreleaser v1.14.1\n\nbefore:\n  hooks:\n    - go mod tidy\n\nchecksum:\n  name_template: 'checksums.txt'\n\nsnapshot:\n  name_template: 'SNAPSHOT-{{ .Commit }}'\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - '^demos:'\n      - '^configs:'\n      - Merge pull request\n      - Merge branch\n      - go mod tidy\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - windows\n      - darwin\n      - freebsd\n    goarch:\n      - amd64\n      - arm64\n      - 386\n    goarm:\n      - 7\n    ignore:\n      - goos: darwin\n        goarch: 386\n      - goos: freebsd\n        goarch: arm64\n    mod_timestamp: \"{{ .CommitTimestamp }}\"\n    ldflags:\n       - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}\n\nnfpms:\n  - package_name: otel-cli\n    homepage: https://github.com/equinix-labs/otel-cli\n    maintainer: Amy Tobey <atobey@equinix.com>\n    description: OpenTelemetry CLI Application (Server & Client)\n    license: Apache 2.0\n    formats:\n      - apk\n      - deb\n      - rpm\n\narchives:\n  - format: tar.gz\n    format_overrides:\n      - goos: windows\n        format: zip\n    builds_info:\n      group: root\n      owner: root\n    rlcp: true\n\nbrews:\n  # This means the repository must be equinix-labs/homebrew-otel-cli\n  - name: \"otel-cli\"\n    url_template: \"https://github.com/equinix-labs/otel-cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}\"\n    tap:\n      owner: \"equinix-labs\"\n      name: \"homebrew-otel-cli\"\n      token: \"{{ .Env.GITHUB_TOKEN }}\"\n    commit_author:\n      name: \"tobert\"\n      email: \"atobey@equinix.com\"\n    homepage: \"https://github.com/equinix-labs/otel-cli\"\n    description: \"OpenTelemetry command-line tool for sending events from shell scripts & similar environments\"\n    license: \"Apache-2.0\"\n    # If set to auto, the release will not be uploaded to the homebrew tap\n    # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1\n    skip_upload: \"auto\"\n\ndockers:\n  - image_templates:\n    - \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-amd64\"\n    dockerfile: release/Dockerfile\n    use: buildx\n    build_flag_templates:\n      - \"--pull\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.name={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n      - \"--label=org.opencontainers.image.source={{.GitURL}}\"\n      - \"--platform=linux/amd64\"\n  - image_templates:\n    - \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-arm64v8\"\n    dockerfile: release/Dockerfile\n    use: buildx\n    build_flag_templates:\n      - \"--pull\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.name={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n      - \"--label=org.opencontainers.image.source={{.GitURL}}\"\n      - \"--platform=linux/arm64/v8\"\n\ndocker_manifests:\n  - name_template: \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}\"\n    image_templates:\n    - \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-amd64\"\n    - \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-arm64v8\"\n  - name_template: \"ghcr.io/equinix-labs/otel-cli:latest\"\n    image_templates:\n    - \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-amd64\"\n    - \"ghcr.io/equinix-labs/otel-cli:{{ .Tag }}-arm64v8\"\n    use: docker\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [0.4.6] - 2024-05-13\n\nBuild smaller binaries and add version subcommand.\n\n### Added\n\n- `otel-cli version` will now report version information of the binary (if any)\n\n### Changed\n\n- linker flags now contain `-s -w` to reduce binary size\n   - change made in build Dockerfile, goreleaser, and GH Actions\n   - contribution from @Ipmi-13, thank you!\n- goreleaser now does the -X flags so `otel-cli version` will work right\n- removed Content-Length from functional tests bc it's not fixed with gzip in play\n- updated demos, added one\n- updated instructions in README.md\n\n## [0.4.5] - 2024-01-01\n\nFix exec attributes, cleanups, and dependency updates.\n\n`otel-cli exec` attributes were broken for the last few releases so @tobert\nfelt it was ok to rename them to match the OTel semantic conventions\nnow.\n\n### Changed\n\n- using latest deps for grpc, protobuf, and otel\n- exec attributes will now go out with the span\n- exec now sends process.command and process.command_args attributes\n- main_test.go now supports regular expression tests on span data\n\n## [0.4.4] - 2024-03-11\n\nFix a typo in the `OTEL_CLI_EXEC_TP_DISABLE_INJECT` envvar.\n\n### Changed\n\n- spell `DISALBE` correctly in `OTEL_CLI_EXEC_TP_DISABLE_INJECT`\n- adds a test for that\n\n## [0.4.3] - 2024-03-11\n\nAdd injection of `{{traceparent}}` to `otel-cli exec` as default behavior, along with\nthe `otel-cli exec --tp-disable-inject` to turn it off (old behavior).\n\n### Added\n\n- `otel-cli exec echo {{traceparent}}` is now supported to pass traceparent to child process\n- `otel-cli exec --tp-disable-inject` will disable this new default behavior\n\n## [0.4.2] - 2023-12-01\n\nThe Docker container now builds off `alpine:latest` instead of `scratch`. This\nmakes the default certificate store included with Alpine available to otel-cli.\n\n### Changed\n\n- switch release Dockerfile to base off alpine:latest\n\n## [0.4.1] - 2023-10-16\n\nMostly small but impactful changes to `otel-cli exec`.\n\n### Added\n\n- `otel-cli exec --command-timeout 30s` provides a separate command timeout from the otel timeout\n- SIGINT is now caught and passed to the child process\n- attributes can be set or overwrite on a backgrounded span via `otel-cli span end`\n\n### Changed\n\n- bumped several dependencies to the latest release\n- updated README.md\n\n## [0.4.0] - 2023-08-09\n\nThis focus of this release is a brand-new OTLP client implementation. It has fewer features\nthan the opentelemetry-collector code, and allows for more fine-grained control over how\ngRPC and HTTP are configured. Along the way, the `otelcli` and `otlpclient` packages went\nthrough a couple refactors to organize code better in preparation for adding metrics and\nlogs, hopefully in 0.5.0.\n\n### Added\n\n- `--force-parent-span-id` allows forcing the span parent (thanks @domofactor!)\n- `otel-cli status` now includes a list of errors including retries that later succeeded\n\n### Changed\n\n- `--otlp-blocking` is marked deprecated and no longer does anything\n- the OTLP client implementation is no longer using opentelemetry-collector\n- traceparent code is now in a w3c/traceparent package\n- otlpserver.CliEvent is removed entirely, preferring protobuf spans & events\n\n## [0.3.0] - 2023-05-26\n\nThe most important change is that `otel-cli exec` now treats arguments as an argv\nlist instead of munging them into a string. This shouldn't break anyone doing sensible\nthings, but could if folks were expecting the old `sh -c` behavior.\n\nEnvvars are no longer deleted before calling exec. Actually, they still are, but otel-cli\nbacks up its envvars early so they can be propagated anyways.\n\nThe rest of the visible changes are incremental additions or fixes. As for the invisible,\notel-cli now generates span protobufs directly, and no longer goes through the\nopentelemetry-go SDK. This ended up making some code cleaner, aside from some of the\nprotobuf-isms that show up.\n\nA user has a use case for setting custom trace and span ids, so the `--force-trace-id`\nand `--force-span-id` options were added to `span`, `exec`, and other span-generating\nsubcommands.\n\n### Added\n\n- `--force-trace-id` and `--force-span-id`\n- `--status-code` and `--status-description` are supported, including on `otel-cli span background end`\n- demo for working around race conditions when using `span background` \n- more functional tests, including some regression tests\n- functional test harness now has a way to do custom checks via CheckFuncs\n- added this changelog\n\n### Changed\n\n- (behavior) reverted envvar deletion so envvars will propagate through `exec` again\n- (behavior) exec argument handling is now precise and no longer uses `sh -c`\n- build now requires Go 1.20 or greater\n- otel-cli now generates span protobufs directly instead of using opentelemetry-go\n- respects signal-specific configs per OTel spec\n- handle endpoint configs closer to spec\n- lots of little cleanups across the code and docs\n- many dependency updates\n\n## [0.2.0] 2023-02-27\n\nThe main addition in this version is client mTLS authentication support, which comes in with\nextensive e2e tests for TLS settings.\n\n`--no-tls-verify` is deprecated in favor of `--tls-no-verify` so all the TLS options are consistent.\n\n`otel-cli span background` now has a `--background-skip-parent-pid-check` option for some use cases\nwhere folks want otel-cli to keep running past its parent process.\n\n### Changed\n\n- 52f1143 #11 support OTEL_SERVICE_NAME per spec (#158)\n- ed4bf2f Bump golang.org/x/net from 0.5.0 to 0.7.0 (#159)\n- 5c5865c Make configurable skipping pid check in span background command (#161)\n- 7214b64 Replace Jaeger instructions with otel-desktop-viewer (#162)\n- 6018f76 TLS naming cleanup (#166)\n- 9a7de86 add TLS testing and client certificate auth (#150)\n- 759fbef miscellaneous documentation fixes (#165)\n- f5286c0 never allow insecure automatically for https:// URIs (#149)\n\n## [0.1.0] 2023-02-02\n\nApologies for the very long delay between releases. There is a lot of pent-up change\nin this release.\n\nBumped minor version to 0.1 because there are some changes in behavior around\nendpoint URI handling and configuration. Also some inconsistencies in command line\narguments has been touched up, so some uses of single-letter flags and `--ignore-tp-env`\n(renamed to `--tp-ignore-env to match other flags) might break.\n\nViper has been dropped in favor of directly loading configuration from json and\nenvironment variables. It appears none of the Viper features ever worked in\notel-cli so it shouldn't be a big deal, but if you were using Viper configs they\nwon't work anymore and you'll have to switch to otel-cli's json config format.\n\nEndpoints now conform mostly to the OTel spec, except for a couple cases\ndocumented in the README.md.\n\n### Changed\n\n- 4256644 #108 fix span background attrs (#116)\n- e8b86f6 #142 follow spec for OTLP protocol (#148)\n- efb5608 #42 add version subcommand (#114)\n- 007f8f7 Add renovate.json (#123)\n- 9d7a668 Make the service/name CLI short args match the long args (#110)\n- b164427 add http testing (#143)\n- 72df644 docs: --ignore-tp-env replace field to --tp-ignore-env (#147)\n- e48e468 feat: add span status code/description cli and env var (#111)\n- ce850f4 make grpc server stop more robust (#122)\n- 8eb37fb remove viper, fix tests, fix and expand envvars (#120)\n- ff5a4eb update OTel to 1.4.1 (#107)\n- b51d6fc update goreleaser config, add release procedure in README.md (#141)\n- 99c9242 update opentelemetry SDK to 1.11.2 (#138)\n\n## [0.0.x] - 2021-03-24 - 2022-02-24\n\nDeveloping the base functionality. Light on testing.\n\n### Changed\n\n- added OTLP test server\n- added goreleaser\n- added timeouts\n- many refactors while discovering the shape of the tool\n- switch to failing silently\n- added subcommand to generate autocomplete data\n- added status subcommand\n- added functional test harness\n- added HTTP support\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:latest AS builder\n\nWORKDIR /build\nCOPY . .\nENV CGO_ENABLED=0\nRUN go build -ldflags=\"-w -s\" -o otel-cli .\n\nFROM scratch AS otel-cli\n\nCOPY --from=builder /build/otel-cli /otel-cli\n\nENTRYPOINT [\"/otel-cli\"]\n\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2020 Packet Host, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# otel-cli\n\n[![](https://img.shields.io/badge/stability-experimental-lightgrey.svg)](https://github.com/packethost/standards/blob/master/experimental-statement.md)\n\notel-cli is a command-line tool for sending OpenTelemetry traces. It is written in\nGo and intended to be used in shell scripts and other places where the best option\navailable for sending spans is executing another program.\n\notel-cli can be added to your scripts with no configuration and it will run as normal\nbut in non-recording mode and will emit no traces. This follows the OpenTelemetry community's\nphilosophy of \"first, do no harm\" and makes it so you can add otel-cli to your code and\nlater turn it on.\n\nSince otel-cli needs to connect to the OTLP endpoint on each run, it is highly recommended\nto use a localhost opentelemetry collector that can buffer spans so that the connection\ncost does not slow down your program too much.\n\n## Getting Started\n\nWe publish a number of package formats for otel-cli, including tar.gz, zip (windows),\napk (Alpine), rpm (Red Hat variants), deb (Debian variants), and a brew tap. These\ncan be found on the repo's [Releases](https://github.com/equinix-labs/otel-cli/releases) page.\n\nOn most platforms the easiest way is a go get:\n\n```shell\ngo install github.com/equinix-labs/otel-cli@latest\n```\n\nDocker images are published for each otel-cli release as well:\n\n```shell\ndocker pull ghcr.io/equinix-labs/otel-cli:latest\ndocker run ghcr.io/equinix-labs/otel-cli:latest status\n```\n\nTo use the brew tap e.g. on MacOS:\n\n```shell\nbrew tap equinix-labs/otel-cli\nbrew install otel-cli\n```\n\nAlternatively, clone the repo and build it locally:\n\n```shell\ngit clone git@github.com:equinix-labs/otel-cli.git\ncd otel-cli\ngo build\n```\n\n## Examples\n\n```shell\n# run otel-cli as a local OTLP server and print traces to your console\n# run this in its own terminal and try some of the commands below!\notel-cli server tui\n\n# configure otel-cli to talk the the local server spawned above\nexport OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317\n\n# run a program inside a span\notel-cli exec --service my-service --name \"curl google\" curl https://google.com\n\n# otel-cli propagates context via envvars so you can chain it to create child spans\notel-cli exec --kind producer -- otel-cli exec --kind consumer sleep 1\n\n# if a traceparent envvar is set it will be automatically picked up and\n# used by span and exec. use --tp-ignore-env to ignore it even when present\nexport TRACEPARENT=00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01\n\n# you can pass the traceparent to a child via arguments as well\n# {{traceparent}} in any of the command's arguments will be replaced with the traceparent string\notel-cli exec --name \"curl api\" -- \\\n   curl -H 'traceparent: {{traceparent}}' https://myapi.com/v1/coolstuff\n\n# create a span with a custom start/end time using either RFC3339,\n# same with the nanosecond extension, or Unix epoch, with/without nanos\notel-cli span --start 2021-03-24T07:28:05.12345Z --end 2021-03-24T07:30:08.0001Z\notel-cli span --start 1616620946 --end 1616620950.241980634\n# so you can do this:\nstart=$(date --rfc-3339=ns) # rfc3339 with nanoseconds\nsome-interesting-program --with-some-options\nend=$(date +%s.%N) # Unix epoch with nanoseconds\notel-cli span -n my-script -s some-interesting-program --start $start --end $end\n\n# for advanced cases you can start a span in the background, and\n# add events to it, finally closing it later in your script\nsockdir=$(mktemp -d)\notel-cli span background \\\n   --service $0          \\\n   --name \"$0 runtime\"   \\\n   --sockdir $sockdir & # the & is important here, background server will block\nsleep 0.1 # give the background server just a few ms to start up\notel-cli span event --name \"cool thing\" --attrs \"foo=bar\" --sockdir $sockdir\notel-cli span end --sockdir $sockdir\n# or you can kill the background process and it will end the span cleanly\nkill %1\n\n# server mode can also write traces to the filesystem, e.g. for testing\ndir=$(mktemp -d)\notel-cli server json --dir $dir --timeout 60 --max-spans 5\n```\n\n## Configuration\n\nEverything is configurable via CLI arguments, json config, and environment\nvariables. If no endpoint is specified, otel-cli will run in non-recording\nmode and not attempt to contact any servers.\n\nAll three modes of config can be mixed. Command line args are loaded first,\nthen config file, then environment variables.\n\n| CLI argument         | environment variable                  | config file key          | example value  |\n| -------------------- | ------------------------------------- | ------------------------ | -------------- |\n| --endpoint           | OTEL_EXPORTER_OTLP_ENDPOINT           | endpoint                 | localhost:4317       |\n| --traces-endpoint    | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT    | traces_endpoint          | https://localhost:4318/v1/traces |\n| --protocol           | OTEL_EXPORTER_OTLP_PROTOCOL           | protocol                 | http/protobuf  |\n| --insecure           | OTEL_EXPORTER_OTLP_INSECURE           | insecure                 | false          |\n| --timeout            | OTEL_EXPORTER_OTLP_TIMEOUT            | timeout                  | 1s             |\n| --otlp-headers       | OTEL_EXPORTER_OTLP_HEADERS            | otlp_headers             | k=v,a=b        |\n| --otlp-blocking      | OTEL_EXPORTER_OTLP_BLOCKING           | otlp_blocking            | false          |\n| --config             | OTEL_CLI_CONFIG_FILE                  | config_file              | config.json    |\n| --verbose            | OTEL_CLI_VERBOSE                      | verbose                  | false          |\n| --fail               | OTEL_CLI_FAIL                         | fail                     | false          |\n| --service            | OTEL_SERVICE_NAME                     | service_name             | myapp          |\n| --kind               | OTEL_CLI_TRACE_KIND                   | span_kind                | server         |\n| --status-code        | OTEL_CLI_STATUS_CODE                  | span_status_code         | error          |\n| --status-description | OTEL_CLI_STATUS_DESCRIPTION           | span_status_description  | cancelled      |\n| --attrs              | OTEL_CLI_ATTRIBUTES                   | span_attributes          | k=v,a=b        |\n| --force-trace-id     | OTEL_CLI_FORCE_TRACE_ID               | force_trace_id           | 00112233445566778899aabbccddeeff |\n| --force-span-id      | OTEL_CLI_FORCE_SPAN_ID                | force_span_id            | beefcafefacedead |\n| --force-parent-span-id | OTEL_CLI_FORCE_PARENT_SPAN_ID       | force_parent_span_id     | eeeeeeb33fc4f3d3 |\n| --tp-required        | OTEL_CLI_TRACEPARENT_REQUIRED         | traceparent_required     | false          |\n| --tp-carrier         | OTEL_CLI_CARRIER_FILE                 | traceparent_carrier_file | filename.txt   |\n| --tp-ignore-env      | OTEL_CLI_IGNORE_ENV                   | traceparent_ignore_env   | false          |\n| --tp-print           | OTEL_CLI_PRINT_TRACEPARENT            | traceparent_print        | false          |\n| --tp-export          | OTEL_CLI_EXPORT_TRACEPARENT           | traceparent_print_export | false          |\n| --tls-no-verify      | OTEL_CLI_TLS_NO_VERIFY                | tls_no_verify    | false                  |\n| --tls-ca-cert        | OTEL_EXPORTER_OTLP_CERTIFICATE        | tls_ca_cert      | /ca/ca.pem             |\n| --tls-client-key     | OTEL_EXPORTER_OTLP_CLIENT_KEY         | tls_client_key   | /keys/client-key.pem   |\n| --tls-client-cert    | OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE | tls_client_cert  | /keys/client-cert.pem  |\n\n[Valid timeout units](https://pkg.go.dev/time#ParseDuration) are \"ns\", \"us\"/\"µs\", \"ms\", \"s\", \"m\", \"h\".\n\n### Endpoint URIs\n\notel-cli deviates from the OTel specification for endpoint URIs. Mainly, otel-cli supports\nbare host:port for grpc endpoints and continues to default to gRPC. The optional http/json\nis not supported by opentelemetry-go so otel-cli does not support it. To use gRPC with an\nhttp endpoint, set the protocol with --protocol or the envvar.\n\n   * bare `host:port` endpoints are assumed to be gRPC and are not supported for HTTP\n   * `http://` and `https://` are assumed to be HTTP unless --protocol is set to `grpc`.\n   * loopback addresses without an https:// prefix are assumed to be unencrypted\n\n### Header and Attribute formatting\n\nHeaders and attributes allow for `key=value,k=v` style formatting. Internally both\notel-cli and pflag use Go's `encoding/csv` to parse these values. Therefore, if you want\nto pass commas in a value, follow CSV quoting rules and quote the whole k=v pair.\nDouble quotes need to be escaped so the shell doesn't interpolate them. Once that's done,\nembedding commas will work fine.\n\n```shell\notel-cli span --attrs item1=value1,\\\"item2=value2,value3\\\",item3=value4\notel-cli span --attrs 'item1=value1,\"item2=value2,value3\",item3=value4'\n```\n\n### Docker TLS Certificates\n\nAs of release 0.4.2, otel-cli containers are built off the latest Alpine base\nimage which contains the base CA certificate bundles. In order to override\nthese for e.g. a self-signed certificate, the best bet is to volume mount your\nown /etc/ssl into the container, and it should get picked up by otel-cli and Go's\nTLS libraries.\n\n```shell\ndocker run -v /etc/ssl:/etc/ssl ghcr.io/equinix-labs/otel-cli:latest status\n```\n\n## Easy local dev\n\nWe want working on otel-cli to be easy, so we've provided a few different ways to get\nstarted. In general, there are three things you need:\n\n- A working Go environment\n- A built (or installed) copy of otel-cli itself\n- A system to receive/inspect the traces you generate\n\n### 1. A working Go environment\n\nProviding instructions on getting Go up and running on your machine is out of scope for this\nREADME. However, the good news is that it's fairly easy to do! You can follow the normal\n[Installation instructions](https://golang.org/doc/install) from the Go project itself.\n\n### 2. A built (or installed) copy of otel-cli itself\n\nIf you're planning on making changes to otel-cli, we recommend building the project locally: `go build`\n\nBut, 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.\n\n### 3. A system to receive/inspect the traces you generate\n\notel-cli can run as a server and accept OTLP connections. It has two modes, one prints to your console\nwhile the other writes to JSON files.\n\n```shell\notel-cli server tui\notel-cli server json --dir $dir --timeout 60 --max-spans 5\n```\n\nMany SaaS vendors accept OTLP these days so one option is to send directly to those. This is not\nrecommended for production since it will slow your code down on the roundtrips. It is recommended\nto use an opentelemetry-collector locally.\n\nAnother option is to use [`otel-desktop-viewer`](https://github.com/CtrlSpice/otel-desktop-viewer). \nThis will bring up a server that can accept OTLP connections.\n\nIf you're not sure what to choose, try `otel-cli server tui` or `otel-desktop-viewer`.\n\n### `otel-desktop-viewer` setup\n\n```shell\n# install the CLI tool\ngo install github.com/CtrlSpice/otel-desktop-viewer@latest\n\n# run it!\n$(go env GOPATH)/bin/otel-desktop-viewer\n\n# if you have $GOPATH/bin added to your $PATH you can call it directly!\notel-desktop-viewer\n\n# if not you can add it to your $PATH by running this or adding it to\n# your startup script (usually ~/.bashrc or ~/.zshrc)\nexport PATH=\"$(go env GOPATH)/bin:$PATH\"\n```\n\nThe OpenTelemetry collector is listening on `localhost:4318`, and the UI will be running on\n`localhost:8000`.\n\n```shell\n# start the desktop viewer (best to do this in a separate terminal)\notel-desktop-viewer\n\n# configure otel-cli to send to our desktop viewer endpoint\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n\n# use otel-cli to generate spans!\notel-cli exec --service my-service --name \"curl google\" curl https://google.com\n```\n\nThis trace will be available at `localhost:8000`.\n\n### SaaS tracing vendor\n\nWe've provided Honeycomb, LightStep, and Elastic configurations that you could also use,\nif you're using one of those vendors today. It's still pretty easy to get started:\n\n```shell\n# optional: to send data to an an OTLP-enabled tracing vendor, pass in your\n# API auth token over an environment variable and modify\n# `local/otel-vendor-config.yaml` according to the comments inside\nexport LIGHTSTEP_TOKEN= # Lightstep API key (otlp/1 in the yaml)\nexport HONEYCOMB_TEAM=  # Honeycomb API key (otlp/2 in the yaml)\nexport ELASTIC_TOKEN= # Elastic token for the APM server.\n\ndocker run \\\n   --env LIGHTSTEP_TOKEN \\\n   --env HONEYCOMB_TEAM \\\n   --env ELASTIC_TOKEN \\\n   --name otel-collector \\\n   --net host \\\n   --volume $(pwd)/configs/otel-vendor-config.yaml:/local.yaml \\\n   otel/opentelemetry-collector-contrib:0.101.0 \\\n      --config /local.yaml\n```\n\nThen it should just work to run otel-cli:\n\n```shell\n./otel-cli span -n \"testing\" -s \"my first test span\"\n# or for quick iterations:\ngo run . span -n \"testing\" -s \"my first test span\"\n```\n\n## Contributing\n\nPlease file issues and PRs on the GitHub project at https://github.com/equinix-labs/otel-cli\n\n## Releases\n\nReleases are managed by goreleaser. Currently this is limited to @tobert due to rules in\nthe equinix-labs organization. For now releases are not automated, but will be by the time\na v1.0 rolls out and the test suite is robust enough that we feel confident.\n\nTesting the release: `goreleaser release --snapshot --rm-dist`\n\nTo release, a GitHub personal access token is required. The release also needs to be tagged\nin git.\n\n```shell\ndocker login ghcr.io # log into GitHub Docker repo\ngh repo list         # make sure GitHub PAT is working\ngit checkout main    # release tags must be off the main branch\ngit pull --rebase    # get the latest HEAD\ngit tag v0.1.1       # tag HEAD with the next version\ngit push --tags      # push new tag up to GitHub\ngoreleaser release --rm-dist\n```\n\n## License\n\nApache 2.0, see LICENSE\n\n"
  },
  {
    "path": "TESTING.md",
    "content": "# Testing otel-cli\n\n## Synopsis\n\notel-cli's primary method of testing is functional, implemented in\n`main_test.go` and accompanying files. It sets up a server and runs an otel-cli\nbuild through a number of tests to verify that everything from environment\nvariable passing to server TLS negotiation work as expected.\n\n## Unit Testing\n\nDo it. It doesn't have to be fancy, just exercise the code a little. It's more\nabout all of us being able to iterate quickly than reaching total coverage.\n\nMost unit tests are in the `otelcli` package. The tests in the root of this\nproject are not unit tests.\n\n## The otel-cli Test Harness\n\nWhen `go test` is run in the root of this project, it runs the available\n`./otel-cli` binary through a suite of tests, providing otel-cli with its\nendpoint information (via templates) and examining the payloads received on the\nserver.\n\nThe otel-cli test harness is more complex than otel-cli itself. Its goal is to\nbe able to test that setting e.g. `OTEL_EXPORTER_OTLP_CLIENT_KEY` works all the\nway through to authenticating to a TLS server. The bugs are going to exist in\nthe glue code, since that's mostly what otel-cli is. Each of Cobra,\n`encoding/json`, and opentelemetry-go are thorougly unit and battle tested. So,\notel-cli tests a build in a functional test harness.\n\nTests are defined in `data_for_test.go` in Go data structures. Suites are a\ngroup of Fixtures that go together. Mostly Suites are necessary for the\nbackgrounding feature, to test e.g. `otel-cli span background`, and to organize\ntests by functionality, etc.. Fixtures configure everything for an otel-cli\ninvocation, and what to expect back from it.\n\nThe OTLP server functionality originally built for `otel-cli server tui` is\nre-used in the tests to run a server in a goroutine. It supports both gRPC and\nHTTP variants of OTLP, and can be configured with TLS. This allows otel-cli to\nconnect to a server and send traces, which the harness then compares to\nexpectations defined in the test Fixture.\n\notel-cli has a special subcommand, `otel-cli status` that sends a span and\nreports back on otel-cli's internal state. The test harness detects status\ncommands and can check data in it.\n\n`tls_for_test.go` implements an ephemeral certificate authority that is created\nand destroyed on each run. The rest of the test harness injects the CA and certs\ncreated into the tests, allowing for full-system testing.\n\nA Fixture can be configured to run \"in the background\". In this case, the harness\nwill behave as if you ran the command `./otel-cli &` and let following fixtures\nrun on top of it. This is mostly used to test `otel-cli span background`, which\nexists primarily to run as a backgrounded job in a shell script. When background\njobs are in use, be careful with test names as they are used as a key to manage\nthe process.\n\n## Adding Tests\n\nFor a new area of functionality, you'll want to add a Suite. A Suite is mostly\nfor organization of tests, but is also used to manage state when testing background\njobs. A Fixture is made of two parts: an otel-cli command configuration, and a\ndata structure of expected results. The harness presents otel-cli with the exact\nARGV specified in `Config.CliArgs`, and a clean environment with only the envvars\nprovided in the `Env` stringmap. The output from otel-cli is captured with stdout\nand stderr combined. This can be tested against as well.\n\nIt is often wise to pass `\"--fail\", \"--verbose\"` to CliArgs for debugging and it's\nfine to leave them on permanently. Without them otel-cli will be silent about\nfailures and you'll get a confusing test output.\n\nMost of the time it's best to copy an existing Suite or Fixture and adjust it to\nthe case you're trying to test. Please try to clean out any unneeded config when\nyou do this so the tests are easy to understand. It's not bad to to test a little\nextra surface area, just try to keep things readable.\n"
  },
  {
    "path": "configs/otel-collector.yaml",
    "content": "---\nreceivers:\n  otlp:\n    protocols:\n      grpc:\n\nexporters:\n  logging:\n    loglevel: debug\n  jaeger:\n    endpoint: \"jaeger:14250\"\n    tls:\n      insecure: true\n\nprocessors:\n  batch:\n\nservice:\n  pipelines:\n    traces:\n      receivers: [otlp]\n      processors: [batch]\n      exporters: [jaeger,logging]\n\n"
  },
  {
    "path": "configs/otel-vendor-config.yaml",
    "content": "# opentelemetry-collector is a proxy for telemetry events.\n#\n# This configuration is set up for use in otel-cli development.\n# With collector in debug mode every trace is printed to the console\n# which makes working on otel-cli quick & easy. There are also\n# examples below for how to send to Lightstep and Honeycomb.\n\nreceivers:\n  otlp:\n    protocols:\n      # OTLP over gRPC\n      grpc:\n        endpoint: \"0.0.0.0:4317\"\n      # OTLP over HTTP (opentelemetry-ruby only works on this proto for now)\n      http:\n        endpoint: \"0.0.0.0:55681\"\n\nprocessors:\n  batch:\n\nexporters:\n  # set to detailed and your traces will get printed to the console spammily\n  debug:\n    verbosity: detailed\n  # Lightstep: set & export LIGHTSTEP_TOKEN and enable below\n  otlp/1:\n    endpoint: \"ingest.lightstep.com:443\"\n    headers:\n      \"lightstep-access-token\": \"${LIGHTSTEP_TOKEN}\"\n  # Honeycomb: set & export HONEYCOMB_TEAM to the auth token\n  # You may also want to set HONEYCOMB_DATASET for legacy accounts.\n  otlp/2:\n    endpoint: \"api.honeycomb.io:443\"\n    headers:\n      \"x-honeycomb-team\": \"${HONEYCOMB_TEAM}\"\n  # Elastic: set & export the ELASTIC_TOKEN to the auth token for the APM server.\n  otlp/3:\n    endpoint: \"xxx.elastic-cloud.com:443\"\n    headers:\n        Authorization: \"Bearer ${ELASTIC_TOKEN}\"\nservice:\n  pipelines:\n    traces:\n      receivers: [otlp]\n      processors: [batch]\n      # only enable debug by default\n      exporters: [debug]\n      # Lightstep:\n      # exporters: [debug, otlp/1]\n      # Honeycomb:\n      # exporters: [debug, otlp/2]\n      # Elastic:\n      # exporters: [debug, otlp/3]\n"
  },
  {
    "path": "data_for_test.go",
    "content": "package main_test\n\n// This file implements data structures and data for functional testing of\n// otel-cli.\n//\n// See: TESTING.md\n//\n// TODO: Results.SpanData could become a struct now\n\nimport (\n\t\"os\"\n\t\"regexp\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/otelcli\"\n\t\"github.com/equinix-labs/otel-cli/otlpclient\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\ntype serverProtocol int\n\nconst (\n\tgrpcProtocol serverProtocol = iota\n\thttpProtocol\n)\n\n// CheckFunc is a function that gets called after the test is run to do\n// custom checking of values.\ntype CheckFunc func(t *testing.T, fixture Fixture, results Results)\n\ntype FixtureConfig struct {\n\tCliArgs []string\n\tEnv     map[string]string\n\t// timeout for how long to wait for the whole test in failure cases\n\tTestTimeoutMs int\n\t// when true this test will be excluded under go -test.short mode\n\t// TODO: maybe move this up to the suite?\n\tIsLongTest bool\n\t// either grpcProtocol or httpProtocol, defaults to grpc\n\tServerProtocol serverProtocol\n\t// sets up the server with the test CA, requiring TLS\n\tServerTLSEnabled bool\n\t// tells the server to require client certificate authentication\n\tServerTLSAuthEnabled bool\n\t// for timeout tests we need to start the server to generate the endpoint\n\t// but do not want it to answer when otel-cli calls, this does that\n\tStopServerBeforeExec bool\n\t// run this fixture in the background, starting its server and otel-cli\n\t// instance, then let those block in the background and continue running\n\t// serial tests until it's \"foreground\" by a second fixtue with the same\n\t// description in the same file\n\tBackground bool\n\tForeground bool\n\t// for testing signal behavior a time to wait before sending a signal\n\t// and the signal to send can be specified\n\tKillAfter  time.Duration\n\tKillSignal os.Signal\n}\n\n// mostly mirrors otelcli.StatusOutput but we need more\ntype Results struct {\n\t// same as otelcli.StatusOutput but copied because embedding doesn't work for this\n\tConfig      otelcli.Config       `json:\"config\"`\n\tSpanData    map[string]string    `json:\"span_data\"`\n\tEnv         map[string]string    `json:\"env\"`\n\tDiagnostics otelcli.Diagnostics  `json:\"diagnostics\"`\n\tErrors      otlpclient.ErrorList `json:\"errors\"`\n\t// these are specific to tests...\n\tServerMeta    map[string]string\n\tHeaders       map[string]string // headers sent by the client\n\tResourceSpans *tracepb.ResourceSpans\n\tCliOutput     string         // merged stdout and stderr\n\tCliOutputRe   *regexp.Regexp // regular expression to clean the output before comparison\n\tSpanCount     int            // number of spans received\n\tEventCount    int            // number of events received\n\tTimedOut      bool           // true when test timed out\n\tCommandFailed bool           // otel-cli was killed, did not exit() on its own\n\tExitCode      int            // the process exit code returned by otel-cli\n\tSpan          *tracepb.Span\n\tSpanEvents    []*tracepb.Span_Event\n}\n\n// Fixture represents a test fixture for otel-cli.\ntype Fixture struct {\n\tName       string\n\tConfig     FixtureConfig\n\tEndpoint   string\n\tTlsData    TlsSettings\n\tExpect     Results\n\tCheckFuncs []CheckFunc\n}\n\n// FixtureSuite is a list of Fixtures that run serially.\ntype FixtureSuite []Fixture\n\nvar suites = []FixtureSuite{\n\t// otel-cli should not do anything when it is not explicitly configured\"\n\t{\n\t\t{\n\t\t\tName: \"nothing configured\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"status\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:     false,\n\t\t\t\t\tNumArgs:         1,\n\t\t\t\t\tParsedTimeoutMs: 1000,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t// setting minimum envvars should result in a span being received\n\t{\n\t\t{\n\t\t\tName: \"minimum configuration (recording, grpc)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\t// otel-cli should NOT set insecure when it auto-detects localhost\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"{{endpoint}}\").\n\t\t\t\t\tWithInsecure(false),\n\t\t\t\tServerMeta: map[string]string{\n\t\t\t\t\t\"proto\": \"grpc\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t}, {\n\t\t\tName: \"minimum configuration (recording, http)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"http://{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\t// otel-cli should NOT set insecure when it auto-detects localhost\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"http://{{endpoint}}\").\n\t\t\t\t\tWithInsecure(false),\n\t\t\t\tServerMeta: map[string]string{\n\t\t\t\t\t\"content-type\": \"application/x-protobuf\",\n\t\t\t\t\t\"host\":         \"{{endpoint}}\",\n\t\t\t\t\t\"method\":       \"POST\",\n\t\t\t\t\t\"proto\":        \"HTTP/1.1\",\n\t\t\t\t\t\"uri\":          \"/v1/traces\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t},\n\t// everything works as expected with fully-loaded options\n\t{\n\t\t{\n\t\t\tName: \"fully loaded command line\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"TRACEPARENT\": \"00-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-AAAAAAAAAAAAAAAA-01\",\n\t\t\t\t},\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"span\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--protocol\", \"grpc\",\n\t\t\t\t\t\"--insecure\",\n\t\t\t\t\t\"--timeout\", \"500000us\", // 500ms\n\t\t\t\t\t\"--fail\", \"--verbose\",\n\t\t\t\t\t\"--name\", \"slartibartfast\",\n\t\t\t\t\t\"--service\", \"Starship Bistromath\",\n\t\t\t\t\t\"--kind\", \"server\",\n\t\t\t\t\t\"--start\", \"308534400\",\n\t\t\t\t\t\"--end\", \"1979-10-12T23:59:59Z\",\n\t\t\t\t\t\"--attrs\", \"protagonist=DentArthurdent,medium=book\",\n\t\t\t\t\t\"--otlp-headers\", \"lue=42\",\n\t\t\t\t\t\"--force-span-id\", \"BBBBBBBBBBBBBBBB\",\n\t\t\t\t\t\"--tp-required\",\n\t\t\t\t\t\"--tp-print\",\n\t\t\t\t\t\"--tp-export\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig(),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:        true,\n\t\t\t\t\tNumArgs:            8,\n\t\t\t\t\tParsedTimeoutMs:    1000,\n\t\t\t\t\tDetectedLocalhost:  true,\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t},\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":    \"*\",\n\t\t\t\t\t\"trace_id\":   \"*\",\n\t\t\t\t\t\"attributes\": `medium=book,protagonist=DentArthurdent`,\n\t\t\t\t},\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\":authority\":   \"{{endpoint}}\\n\",\n\t\t\t\t\t\"content-type\": \"application/grpc\\n\",\n\t\t\t\t\t\"user-agent\":   \"*\",\n\t\t\t\t\t\"lue\":          \"42\\n\",\n\t\t\t\t},\n\t\t\t\tCliOutput: \"\" +\n\t\t\t\t\t\"# trace id: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\n\" +\n\t\t\t\t\t\"#  span id: bbbbbbbbbbbbbbbb\\n\" +\n\t\t\t\t\t\"export TRACEPARENT=00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01\\n\",\n\t\t\t},\n\t\t},\n\t},\n\t// TLS connections\n\t{\n\t\t{\n\t\t\tName: \"minimum configuration (tls, no-verify, recording, grpc)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"status\",\n\t\t\t\t\t\"--endpoint\", \"https://{{endpoint}}\",\n\t\t\t\t\t\"--protocol\", \"grpc\",\n\t\t\t\t\t// TODO: switch to --tls-no-verify before 1.0, for now keep testing it\n\t\t\t\t\t\"--verbose\", \"--fail\", \"--no-tls-verify\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs:    1000,\n\t\t\t\tServerTLSEnabled: true,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"https://{{endpoint}}\").\n\t\t\t\t\tWithProtocol(\"grpc\").\n\t\t\t\t\tWithVerbose(true).\n\t\t\t\t\tWithTlsNoVerify(true),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:        true,\n\t\t\t\t\tNumArgs:            8,\n\t\t\t\t\tDetectedLocalhost:  true,\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t\tParsedTimeoutMs:    1000,\n\t\t\t\t\tEndpoint:           \"*\",\n\t\t\t\t\tEndpointSource:     \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"minimum configuration (tls, no-verify, recording, https)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol:   httpProtocol,\n\t\t\t\tCliArgs:          []string{\"status\", \"--endpoint\", \"https://{{endpoint}}\", \"--tls-no-verify\"},\n\t\t\t\tTestTimeoutMs:    2000,\n\t\t\t\tServerTLSEnabled: true,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\t// otel-cli should NOT set insecure when it auto-detects localhost\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithTlsNoVerify(true).\n\t\t\t\t\tWithEndpoint(\"https://{{endpoint}}\"),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           4,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"minimum configuration (tls, client cert auth, recording, grpc)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"status\",\n\t\t\t\t\t\"--endpoint\", \"https://{{endpoint}}\",\n\t\t\t\t\t\"--protocol\", \"grpc\",\n\t\t\t\t\t\"--verbose\", \"--fail\",\n\t\t\t\t\t\"--tls-ca-cert\", \"{{tls_ca_cert}}\",\n\t\t\t\t\t\"--tls-client-cert\", \"{{tls_client_cert}}\",\n\t\t\t\t\t\"--tls-client-key\", \"{{tls_client_key}}\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs:        1000,\n\t\t\t\tServerTLSEnabled:     true,\n\t\t\t\tServerTLSAuthEnabled: true,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"https://{{endpoint}}\").\n\t\t\t\t\tWithProtocol(\"grpc\").\n\t\t\t\t\tWithTlsCACert(\"{{tls_ca_cert}}\").\n\t\t\t\t\tWithTlsClientKey(\"{{tls_client_key}}\").\n\t\t\t\t\tWithTlsClientCert(\"{{tls_client_cert}}\").\n\t\t\t\t\tWithVerbose(true),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:        true,\n\t\t\t\t\tNumArgs:            13,\n\t\t\t\t\tDetectedLocalhost:  true,\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t\tParsedTimeoutMs:    1000,\n\t\t\t\t\tEndpoint:           \"*\",\n\t\t\t\t\tEndpointSource:     \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"minimum configuration (tls, client cert auth, recording, https)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"status\",\n\t\t\t\t\t\"--endpoint\", \"https://{{endpoint}}\",\n\t\t\t\t\t\"--verbose\", \"--fail\",\n\t\t\t\t\t\"--tls-ca-cert\", \"{{tls_ca_cert}}\",\n\t\t\t\t\t\"--tls-client-cert\", \"{{tls_client_cert}}\",\n\t\t\t\t\t\"--tls-client-key\", \"{{tls_client_key}}\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs:        2000,\n\t\t\t\tServerTLSEnabled:     true,\n\t\t\t\tServerTLSAuthEnabled: true,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"https://{{endpoint}}\").\n\t\t\t\t\tWithTlsCACert(\"{{tls_ca_cert}}\").\n\t\t\t\t\tWithTlsClientKey(\"{{tls_client_key}}\").\n\t\t\t\t\tWithTlsClientCert(\"{{tls_client_cert}}\").\n\t\t\t\t\tWithVerbose(true),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           11,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t},\n\t// ensure things fail when they're supposed to fail\n\t{\n\t\t// otel is configured but there is no server listening so it should time out silently\n\t\t{\n\t\t\tName: \"timeout with no server\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--timeout\", \"1s\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t},\n\t\t\t\t// this needs to be less than the timeout in CliArgs\n\t\t\t\tTestTimeoutMs:        500,\n\t\t\t\tIsLongTest:           true, // can be skipped with `go test -short`\n\t\t\t\tStopServerBeforeExec: true, // there will be no server listening\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\t// we want and expect a timeout and failure\n\t\t\t\tTimedOut:      true,\n\t\t\t\tCommandFailed: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"syntax errors in environment variables cause the command to fail\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--fail\", \"--verbose\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t\t\"OTEL_CLI_VERBOSE\":            \"lmao\", // invalid input\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig:   otelcli.DefaultConfig(),\n\t\t\t\tExitCode: 1,\n\t\t\t\t// strips the date off the log line before comparing to expectation\n\t\t\t\tCliOutputRe: regexp.MustCompile(`^\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} `),\n\t\t\t\tCliOutput: \"Error while loading environment variables: could not parse OTEL_CLI_VERBOSE value \" +\n\t\t\t\t\t\"\\\"lmao\\\" as an bool: strconv.ParseBool: parsing \\\"lmao\\\": invalid syntax\\n\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"https:// should fail when TLS is not available\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"https://{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"https://{{endpoint}}\"),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 0,\n\t\t\t},\n\t\t\tCheckFuncs: []CheckFunc{\n\t\t\t\tfunc(t *testing.T, f Fixture, r Results) {\n\t\t\t\t\twant := injectVars(`Post \"https://{{endpoint}}/v1/traces\": http: server gave HTTP response to HTTPS client`, f.Endpoint, f.TlsData)\n\t\t\t\t\tif len(r.Errors) >= 1 {\n\t\t\t\t\t\tif r.Errors[0].Error != want {\n\t\t\t\t\t\t\tt.Errorf(\"Got the wrong error: %q\", r.Errors[0].Error)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Errorf(\"Expected at least one error but got %d.\", len(r.Errors))\n\t\t\t\t\t}\n\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\n\t// regression tests\n\t{\n\t\t{\n\t\t\t// The span end time was missing when #175 merged, which showed up\n\t\t\t// as 0ms spans. CheckFuncs was added to make this possible. This\n\t\t\t// test runs sleep for 10ms and checks the duration of the span is\n\t\t\t// at least 10ms.\n\t\t\tName: \"#189 otel-cli exec sets span start time earlier than end time\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\t// note: relies on system sleep command supporting floats\n\t\t\t\t// note: 10ms is hardcoded in a few places for this test and commentary\n\t\t\t\tCliArgs: []string{\"exec\", \"--endpoint\", \"{{endpoint}}\", \"sleep\", \"0.01\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"grpc://{{endpoint}}\"),\n\t\t\t},\n\t\t\tCheckFuncs: []CheckFunc{\n\t\t\t\tfunc(t *testing.T, f Fixture, r Results) {\n\t\t\t\t\t//elapsed := r.Span.End.Sub(r.Span.Start)\n\t\t\t\t\telapsed := time.Duration((r.Span.EndTimeUnixNano - r.Span.StartTimeUnixNano) * uint64(time.Nanosecond))\n\t\t\t\t\tif elapsed.Milliseconds() < 10 {\n\t\t\t\t\t\tt.Errorf(\"elapsed test time not long enough. Expected 10ms, got %d ms\", elapsed.Milliseconds())\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"#181 OTEL_ envvars should persist through to otel-cli exec\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"status\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_FAKE_VARIABLE\":             \"fake value\",\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\":    \"{{endpoint}}\",\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_CERTIFICATE\": \"{{tls_ca_cert}}\",\n\t\t\t\t\t\"X_WHATEVER\":                     \"whatever\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\").WithTlsCACert(\"{{tls_ca_cert}}\"),\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_FAKE_VARIABLE\":             \"fake value\",\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\":    \"{{endpoint}}\",\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_CERTIFICATE\": \"{{tls_ca_cert}}\",\n\t\t\t\t\t\"X_WHATEVER\":                     \"whatever\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tNumArgs:           1,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"#200 custom trace path in general endpoint gets signal path appended\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"http://{{endpoint}}/mycollector\"},\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"http://{{endpoint}}/mycollector\"),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\t// spec says /v1/traces should get appended to any general endpoint URL\n\t\t\t\t\tEndpoint:       \"http://{{endpoint}}/mycollector/v1/traces\",\n\t\t\t\t\tEndpointSource: \"general\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"#200 custom trace path on signal endpoint does not get modified\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:        []string{\"status\", \"--traces-endpoint\", \"http://{{endpoint}}/mycollector/x/1\"},\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithTracesEndpoint(\"http://{{endpoint}}/mycollector/x/1\"),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"http://{{endpoint}}/mycollector/x/1\",\n\t\t\t\t\tEndpointSource:    \"signal\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"#258 Commands that exit with a non-zero exit code should report a span\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"exec\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--verbose\", \"--fail\",\n\t\t\t\t\t\"--\", \"false\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tExitCode:      1,\n\t\t\t\tSpanCount:     1,\n\t\t\t\tCliOutput:     \"\",\n\t\t\t\tCommandFailed: false, // otel-cli should exit voluntarily in this case\n\t\t\t\tConfig:        otelcli.DefaultConfig().WithEndpoint(\"grpc://{{endpoint}}\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"#316 ensure process command and args are sent as attributes\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"exec\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--verbose\", \"--fail\",\n\t\t\t\t\t\"--attrs\", \"zy=ab\", // ensure CLI args still propagate\n\t\t\t\t\t\"--\", \"/bin/echo\", \"a\", \"z\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tCliOutput: \"a z\\n\",\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"attributes\": \"/^process.command=/bin/echo,process.command_args=/bin/echo,a,z,process.owner=\\\\w+,process.parent_pid=\\\\d+,process.pid=\\\\d+,zy=ab/\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli span with no OTLP config should do and print nothing\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span (unconfigured, non-recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--service\", \"main_test.go\", \"--name\", \"test-span-123\", \"--kind\", \"server\"},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t},\n\t// config file\n\t{\n\t\t{\n\t\t\tName: \"load a json config file\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"status\", \"--config\", \"example-config.json\"},\n\t\t\t\t// this will take priority over the config\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tError:             \"could not open file '/tmp/traceparent.txt' for read: open /tmp/traceparent.txt: no such file or directory\",\n\t\t\t\t},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t},\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"{{endpoint}}\"). // tells the test framework to ignore/overwrite\n\t\t\t\t\tWithTimeout(\"1s\").\n\t\t\t\t\tWithHeaders(map[string]string{\"header1\": \"header1-value\"}).\n\t\t\t\t\tWithInsecure(true).\n\t\t\t\t\tWithBlocking(false).\n\t\t\t\t\tWithTlsNoVerify(true).\n\t\t\t\t\tWithTlsCACert(\"/dev/null\").\n\t\t\t\t\tWithTlsClientKey(\"/dev/null\").\n\t\t\t\t\tWithTlsClientCert(\"/dev/null\").\n\t\t\t\t\tWithServiceName(\"configured_in_config_file\").\n\t\t\t\t\tWithSpanName(\"config_file_span\").\n\t\t\t\t\tWithKind(\"server\").\n\t\t\t\t\tWithAttributes(map[string]string{\"attr1\": \"value1\"}).\n\t\t\t\t\tWithStatusCode(\"0\").\n\t\t\t\t\tWithStatusDescription(\"status description\").\n\t\t\t\t\tWithTraceparentCarrierFile(\"/tmp/traceparent.txt\").\n\t\t\t\t\tWithTraceparentIgnoreEnv(true).\n\t\t\t\t\tWithTraceparentPrint(true).\n\t\t\t\t\tWithTraceparentPrintExport(true).\n\t\t\t\t\tWithTraceparentRequired(false).\n\t\t\t\t\tWithBackgroundParentPollMs(100).\n\t\t\t\t\tWithBackgroundSockdir(\"/tmp\").\n\t\t\t\t\tWithBackgroundWait(true).\n\t\t\t\t\tWithSpanEndTime(\"now\").\n\t\t\t\t\tWithSpanEndTime(\"now\").\n\t\t\t\t\tWithEventName(\"config_file_event\").\n\t\t\t\t\tWithEventTime(\"now\").\n\t\t\t\t\tWithCfgFile(\"example-config.json\").\n\t\t\t\t\tWithVerbose(true).\n\t\t\t\t\tWithFail(true),\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli with minimal config span sends a span that looks right\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span (recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--service\", \"main_test.go\", \"--name\", \"test-span-123\", \"--kind\", \"server\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig:    otelcli.DefaultConfig(),\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t// OTEL_RESOURCE_ATTRIBUTES and OTEL_CLI_SERVICE_NAME should get merged into\n\t\t// the span resource attributes\n\t\t{\n\t\t\tName: \"otel-cli span with envvar service name and attributes (recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--name\", \"test-span-service-name-and-attrs\", \"--kind\", \"server\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t\t\"OTEL_CLI_SERVICE_NAME\":       \"test-service-abc123\",\n\t\t\t\t\t\"OTEL_CLI_ATTRIBUTES\":         \"cafe=deadbeef,abc=123\",\n\t\t\t\t\t\"OTEL_RESOURCE_ATTRIBUTES\":    \"foo.bar=baz\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":            \"*\",\n\t\t\t\t\t\"trace_id\":           \"*\",\n\t\t\t\t\t\"attributes\":         \"abc=123,cafe=deadbeef\",\n\t\t\t\t\t\"service_attributes\": \"foo.bar=baz,service.name=test-service-abc123\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t// OTEL_SERVICE_NAME\n\t\t{\n\t\t\tName: \"otel-cli span with envvar service name (recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t\t\"OTEL_SERVICE_NAME\":           \"test-service-123abc\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"service_attributes\": \"service.name=test-service-123abc\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli span --print-tp actually prints\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span --print-tp (non-recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--tp-print\"},\n\t\t\t\tEnv:     map[string]string{\"TRACEPARENT\": \"00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-01\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tCliOutput: \"\" + // empty so the text below can indent and line up\n\t\t\t\t\t\"# trace id: f6c109f48195b451c4def6ab32f47b61\\n\" +\n\t\t\t\t\t\"#  span id: a5d2a35f2483004e\\n\" +\n\t\t\t\t\t\"TRACEPARENT=00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-01\\n\",\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli span --print-tp propagates traceparent even when not recording\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span --tp-print --tp-export (non-recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"--tp-print\", \"--tp-export\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"TRACEPARENT\": \"00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-00\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tCliOutput: \"\" +\n\t\t\t\t\t\"# trace id: f6c109f48195b451c4def6ab32f47b61\\n\" +\n\t\t\t\t\t\"#  span id: a5d2a35f2483004e\\n\" +\n\t\t\t\t\t\"export TRACEPARENT=00-f6c109f48195b451c4def6ab32f47b61-a5d2a35f2483004e-00\\n\",\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli span background, non-recording, this uses the suite functionality\n\t// and background tasks, which are a little clunky but get the job done\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span background (nonrecording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"span\", \"background\", \"--timeout\", \"1s\", \"--sockdir\", \".\"},\n\t\t\t\tTestTimeoutMs: 2000,\n\t\t\t\tBackground:    true,  // sorta like & in shell\n\t\t\t\tForeground:    false, // must be true later, like `fg` in shell\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span event\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"event\", \"--name\", \"an event happened\", \"--attrs\", \"ima=now,mondai=problem\", \"--sockdir\", \".\"},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"end\", \"--sockdir\", \".\"},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\t// Name on foreground *must* match the backgrounded job\n\t\t\t// TODO: ^^ this isn't great, find a better way\n\t\t\tName: \"otel-cli span background (nonrecording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tForeground: true, // bring it back (fg) and finish up\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t},\n\t// otel-cli span background, in recording mode\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span background (recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"span\", \"background\", \"--timeout\", \"1s\", \"--sockdir\", \".\", \"--attrs\", \"abc=def\"},\n\t\t\t\tEnv:           map[string]string{\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs: 2000,\n\t\t\t\tBackground:    true,\n\t\t\t\tForeground:    false,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":    \"*\",\n\t\t\t\t\t\"trace_id\":   \"*\",\n\t\t\t\t\t\"attributes\": `abc=def`, // weird format because of limitation in OTLP server\n\t\t\t\t},\n\t\t\t\tSpanCount:  1,\n\t\t\t\tEventCount: 1,\n\t\t\t},\n\t\t\t// this validates options sent to otel-cli span end\n\t\t\tCheckFuncs: []CheckFunc{\n\t\t\t\tfunc(t *testing.T, f Fixture, r Results) {\n\t\t\t\t\tif r.Span.Status.GetCode() != 2 {\n\t\t\t\t\t\tt.Errorf(\"expected 2 for span status code, but got %d\", r.Span.Status.GetCode())\n\t\t\t\t\t}\n\t\t\t\t\tif r.Span.Status.GetMessage() != \"I can't do that Dave.\" {\n\t\t\t\t\t\tt.Errorf(\"got wrong string for status description: %q\", r.Span.Status.GetMessage())\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span event\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"span\", \"event\", \"--name\", \"an event happened\", \"--attrs\", \"ima=now,mondai=problem\", \"--sockdir\", \".\"},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"span\", \"end\",\n\t\t\t\t\t\"--sockdir\", \".\",\n\t\t\t\t\t// these are validated by checkfuncs defined above ^^\n\t\t\t\t\t\"--status-code\", \"error\",\n\t\t\t\t\t\"--status-description\", \"I can't do that Dave.\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span background (recording)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tForeground: true, // fg\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t},\n\t// otel-cli span background, add attrs on span end\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span background (recording) with attrs added on end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"span\", \"background\", \"--timeout\", \"1s\", \"--sockdir\", \".\"},\n\t\t\t\tEnv:           map[string]string{\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs: 2000,\n\t\t\t\tBackground:    true,\n\t\t\t\tForeground:    false,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":    \"*\",\n\t\t\t\t\t\"trace_id\":   \"*\",\n\t\t\t\t\t\"attributes\": `abc=def,ghi=jkl`, // weird format because of limitation in OTLP server\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"span\", \"end\",\n\t\t\t\t\t\"--sockdir\", \".\",\n\t\t\t\t\t\"--attrs\", \"ghi=jkl,abc=def\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span background (recording) with attrs added on end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tForeground: true, // fg\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t},\n\t// otel-cli span background with attrs, append attrs on span end\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span background (recording) with attrs append on end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"span\", \"background\", \"--timeout\", \"1s\", \"--sockdir\", \".\", \"--attrs\", \"abc=def\"},\n\t\t\t\tEnv:           map[string]string{\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs: 2000,\n\t\t\t\tBackground:    true,\n\t\t\t\tForeground:    false,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":    \"*\",\n\t\t\t\t\t\"trace_id\":   \"*\",\n\t\t\t\t\t\"attributes\": `abc=def,ghi=jkl`, // weird format because of limitation in OTLP server\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"span\", \"end\",\n\t\t\t\t\t\"--sockdir\", \".\",\n\t\t\t\t\t\"--attrs\", \"ghi=jkl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span background (recording) with attrs append on end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tForeground: true, // fg\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t},\n\t// otel-cli span background, modify and add attrs on span end\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span background (recording) with attrs modified and added on end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"span\", \"background\", \"--timeout\", \"1s\", \"--sockdir\", \".\", \"--attrs\", \"abc=123\"},\n\t\t\t\tEnv:           map[string]string{\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs: 2000,\n\t\t\t\tBackground:    true,\n\t\t\t\tForeground:    false,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":    \"*\",\n\t\t\t\t\t\"trace_id\":   \"*\",\n\t\t\t\t\t\"attributes\": `abc=def,ghi=jkl`, // weird format because of limitation in OTLP server\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"span\", \"end\",\n\t\t\t\t\t\"--sockdir\", \".\",\n\t\t\t\t\t\"--attrs\", \"ghi=jkl,abc=def\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli span background (recording) with attrs modified and added on end\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tForeground: true, // fg\n\t\t\t},\n\t\t\tExpect: Results{Config: otelcli.DefaultConfig()},\n\t\t},\n\t},\n\t// otel-cli exec runs echo\n\t{\n\t\t{\n\t\t\tName: \"otel-cli span exec echo\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\t// intentionally calling a command with no args bc it's a special case in exec.go\n\t\t\t\tCliArgs: []string{\"exec\", \"--service\", \"main_test.go\", \"--name\", \"test-span-123\", \"--kind\", \"server\", \"echo\"},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_ENDPOINT\": \"{{endpoint}}\",\n\t\t\t\t\t\"TRACEPARENT\":                 \"00-edededededededededededededed9000-edededededededed-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig(),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"span_id\":  \"*\",\n\t\t\t\t\t\"trace_id\": \"edededededededededededededed9000\",\n\t\t\t\t},\n\t\t\t\tCliOutput: \"\\n\",\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli exec runs otel-cli exec\n\t{\n\t\t{\n\t\t\tName: \"otel-cli exec (nested)\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"exec\", \"--name\", \"outer\", \"--endpoint\", \"{{endpoint}}\", \"--fail\", \"--verbose\", \"--\",\n\t\t\t\t\t\"./otel-cli\", \"exec\", \"--name\", \"inner\", \"--endpoint\", \"{{endpoint}}\", \"--tp-required\", \"--fail\", \"--verbose\", \"echo\", \"hello world\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig:    otelcli.DefaultConfig(),\n\t\t\t\tCliOutput: \"hello world\\n\",\n\t\t\t\tSpanCount: 2,\n\t\t\t},\n\t\t},\n\t},\n\t// otel-cli exec echo \"{{traceparent}}\" and otel-cli exec --tp-disable-inject\n\t{\n\t\t{\n\t\t\tName: \"otel-cli exec with arg injection injects the traceparent\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"exec\", \"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--force-trace-id\", \"e39280f2980af3a8600ae98c74f2dabf\", \"--force-span-id\", \"023eee2731392b4d\",\n\t\t\t\t\t\"--\",\n\t\t\t\t\t\"echo\", \"{{traceparent}}\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tCliOutput: \"00-e39280f2980af3a8600ae98c74f2dabf-023eee2731392b4d-01\\n\",\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli exec --tp-disable-inject returns the {{traceparent}} tag unmodified\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"exec\", \"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--force-trace-id\", \"e39280f2980af3a8600ae98c74f2dabf\", \"--force-span-id\", \"023eee2731392b4d\",\n\t\t\t\t\t\"--tp-disable-inject\",\n\t\t\t\t\t\"--\",\n\t\t\t\t\t\"echo\", \"{{traceparent}}\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tCliOutput: \"{{traceparent}}\\n\",\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"otel-cli exec returns the {{traceparent}} tag unmodified with OTEL_CLI_EXEC_TP_DISABLE_INJECT\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_CLI_EXEC_TP_DISABLE_INJECT\": \"true\",\n\t\t\t\t},\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"exec\", \"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--force-trace-id\", \"e39280f2980af3a8600ae98c74f2dabf\", \"--force-span-id\", \"023eee2731392b4d\",\n\t\t\t\t\t\"--\",\n\t\t\t\t\t\"echo\", \"{{traceparent}}\"},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tCliOutput: \"{{traceparent}}\\n\",\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t},\n\t// validate OTEL_EXPORTER_OTLP_PROTOCOL / --protocol\n\t{\n\t\t// --protocol\n\t\t{\n\t\t\tName: \"--protocol grpc\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"{{endpoint}}\", \"--protocol\", \"grpc\"},\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\").WithProtocol(\"grpc\"),\n\t\t\t\tServerMeta: map[string]string{\n\t\t\t\t\t\"proto\": \"grpc\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           5,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"--protocol http/protobuf\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"http://{{endpoint}}\", \"--protocol\", \"http/protobuf\"},\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().WithEndpoint(\"http://{{endpoint}}\").WithProtocol(\"http/protobuf\"),\n\t\t\t\tServerMeta: map[string]string{\n\t\t\t\t\t\"content-type\": \"application/x-protobuf\",\n\t\t\t\t\t\"host\":         \"{{endpoint}}\",\n\t\t\t\t\t\"method\":       \"POST\",\n\t\t\t\t\t\"proto\":        \"HTTP/1.1\",\n\t\t\t\t\t\"uri\":          \"/v1/traces\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           5,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"protocol: bad config\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"status\", \"--endpoint\", \"{{endpoint}}\", \"--protocol\", \"xxx\", \"--verbose\", \"--fail\"},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tCliOutputRe: regexp.MustCompile(`^\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} `),\n\t\t\t\tCliOutput:   \"invalid protocol setting \\\"xxx\\\"\\n\",\n\t\t\t\tConfig:      otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       false,\n\t\t\t\t\tNumArgs:           7,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t\tExecExitCode:      1,\n\t\t\t\t},\n\t\t\t\tSpanCount: 0,\n\t\t\t},\n\t\t},\n\t\t// OTEL_EXPORTER_OTLP_PROTOCOL\n\t\t{\n\t\t\tName: \"OTEL_EXPORTER_OTLP_PROTOCOL grpc\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t\t// validate protocol can be set to grpc with an http endpoint\n\t\t\t\tCliArgs:       []string{\"status\", \"--endpoint\", \"http://{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_PROTOCOL\": \"grpc\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().WithEndpoint(\"http://{{endpoint}}\").WithProtocol(\"grpc\"),\n\t\t\t\tServerMeta: map[string]string{\n\t\t\t\t\t\"proto\": \"grpc\",\n\t\t\t\t},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_PROTOCOL\": \"grpc\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t\tCliArgs:        []string{\"status\", \"--endpoint\", \"http://{{endpoint}}\"},\n\t\t\t\tTestTimeoutMs:  1000,\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_PROTOCOL\": \"http/protobuf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().WithEndpoint(\"http://{{endpoint}}\").WithProtocol(\"http/protobuf\"),\n\t\t\t\tServerMeta: map[string]string{\n\t\t\t\t\t\"content-type\": \"application/x-protobuf\",\n\t\t\t\t\t\"host\":         \"{{endpoint}}\",\n\t\t\t\t\t\"method\":       \"POST\",\n\t\t\t\t\t\"proto\":        \"HTTP/1.1\",\n\t\t\t\t\t\"uri\":          \"/v1/traces\",\n\t\t\t\t},\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_PROTOCOL\": \"http/protobuf\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"OTEL_EXPORTER_OTLP_PROTOCOL: bad config\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"status\", \"--endpoint\", \"http://{{endpoint}}\", \"--fail\", \"--verbose\"},\n\t\t\t\tTestTimeoutMs: 1000,\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"OTEL_EXPORTER_OTLP_PROTOCOL\": \"roflcopter\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tExitCode:    1,\n\t\t\t\tCliOutputRe: regexp.MustCompile(`^\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} `),\n\t\t\t\tCliOutput:   \"invalid protocol setting \\\"roflcopter\\\"\\n\",\n\t\t\t\tConfig:      otelcli.DefaultConfig().WithEndpoint(\"http://{{endpoint}}\"),\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       false,\n\t\t\t\t\tNumArgs:           3,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t\tError:             \"invalid protocol setting \\\"roflcopter\\\"\\n\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 0,\n\t\t\t},\n\t\t},\n\t},\n\t// --force-trace-id, --force-span-id and --force-parent-span-id allow setting/forcing custom trace, span and parent span ids\n\t{\n\t\t{\n\t\t\tName: \"forced trace, span and parent span ids\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"status\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--fail\",\n\t\t\t\t\t\"--force-trace-id\", \"00112233445566778899aabbccddeeff\",\n\t\t\t\t\t\"--force-span-id\", \"beefcafefacedead\",\n\t\t\t\t\t\"--force-parent-span-id\", \"e4e3eeb33fc4f3d3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tConfig: otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"trace_id\":       \"00112233445566778899aabbccddeeff\",\n\t\t\t\t\t\"span_id\":        \"beefcafefacedead\",\n\t\t\t\t\t\"parent_span_id\": \"e4e3eeb33fc4f3d3\",\n\t\t\t\t},\n\t\t\t\tSpanCount: 1,\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tNumArgs:           10,\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"*\",\n\t\t\t\t\tEndpointSource:    \"*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t// full-system test --otlp-headers makes it to grpc/http servers\n\t{\n\t\t{\n\t\t\tName: \"#231 gRPC headers for authentication\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"status\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--protocol\", \"grpc\",\n\t\t\t\t\t\"--otlp-headers\", \"x-otel-cli-otlpserver-token=abcdefgabcdefg\",\n\t\t\t\t},\n\t\t\t\tServerProtocol: grpcProtocol,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"{{endpoint}}\").\n\t\t\t\t\tWithProtocol(\"grpc\").\n\t\t\t\t\tWithHeaders(map[string]string{\n\t\t\t\t\t\t\"x-otel-cli-otlpserver-token\": \"abcdefgabcdefg\",\n\t\t\t\t\t}),\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\":authority\":                  \"{{endpoint}}\\n\",\n\t\t\t\t\t\"content-type\":                \"application/grpc\\n\",\n\t\t\t\t\t\"user-agent\":                  \"*\",\n\t\t\t\t\t\"x-otel-cli-otlpserver-token\": \"abcdefgabcdefg\\n\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tNumArgs:           7,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"grpc://{{endpoint}}\",\n\t\t\t\t\tEndpointSource:    \"general\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"#231 http headers for authentication\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\n\t\t\t\t\t\"status\",\n\t\t\t\t\t\"--endpoint\", \"http://{{endpoint}}\",\n\t\t\t\t\t\"--protocol\", \"http/protobuf\",\n\t\t\t\t\t\"--otlp-headers\", \"x-otel-cli-otlpserver-token=abcdefgabcdefg\",\n\t\t\t\t},\n\t\t\t\tServerProtocol: httpProtocol,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig: otelcli.DefaultConfig().\n\t\t\t\t\tWithEndpoint(\"http://{{endpoint}}\").\n\t\t\t\t\tWithProtocol(\"http/protobuf\").\n\t\t\t\t\tWithHeaders(map[string]string{\n\t\t\t\t\t\t\"x-otel-cli-otlpserver-token\": \"abcdefgabcdefg\",\n\t\t\t\t\t}),\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Content-Type\":                \"application/x-protobuf\",\n\t\t\t\t\t\"Accept-Encoding\":             \"gzip\",\n\t\t\t\t\t\"User-Agent\":                  \"Go-http-client/1.1\",\n\t\t\t\t\t\"X-Otel-Cli-Otlpserver-Token\": \"abcdefgabcdefg\",\n\t\t\t\t},\n\t\t\t\tDiagnostics: otelcli.Diagnostics{\n\t\t\t\t\tIsRecording:       true,\n\t\t\t\t\tDetectedLocalhost: true,\n\t\t\t\t\tNumArgs:           7,\n\t\t\t\t\tParsedTimeoutMs:   1000,\n\t\t\t\t\tEndpoint:          \"http://{{endpoint}}/v1/traces\",\n\t\t\t\t\tEndpointSource:    \"general\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t// exec signal and timeout behavior\n\t{\n\t\t{\n\t\t\tName: \"handle ctrl-c gracefully\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs:       []string{\"exec\", \"--endpoint\", \"{{endpoint}}\", \"--timeout\", \"2s\", \"sleep\", \"1\"},\n\t\t\t\tKillAfter:     time.Millisecond * 20,\n\t\t\t\tKillSignal:    syscall.SIGINT, // control-c\n\t\t\t\tTestTimeoutMs: 50,             // if we get to 50ms the signal failed\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tExitCode:  2,\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"status_code\":        \"2\",\n\t\t\t\t\t\"status_description\": \"exec command failed: signal: interrupt\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"exec --command-timeout terminates processes\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"exec\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--command-timeout\", \"20ms\",\n\t\t\t\t\t\"sleep\", \"1\",\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs: 50,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tExitCode:  2,\n\t\t\t\tSpanData: map[string]string{\n\t\t\t\t\t\"status_code\":        \"2\",\n\t\t\t\t\t\"status_description\": \"exec command failed: signal: killed\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"exec --command-timeout can run longer than --timeout\",\n\t\t\tConfig: FixtureConfig{\n\t\t\t\tCliArgs: []string{\"exec\",\n\t\t\t\t\t\"--endpoint\", \"{{endpoint}}\",\n\t\t\t\t\t\"--command-timeout\", \"100ms\",\n\t\t\t\t\t\"--timeout\", \"50ms\",\n\t\t\t\t\t\"sleep\", \"0.75\", // depends on GNU sleep's floating point sleeps\n\t\t\t\t},\n\t\t\t\tTestTimeoutMs: 200,\n\t\t\t},\n\t\t\tExpect: Results{\n\t\t\t\tSpanCount: 1,\n\t\t\t\tConfig:    otelcli.DefaultConfig().WithEndpoint(\"{{endpoint}}\"),\n\t\t\t\tExitCode:  0,\n\t\t\t},\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "demos/01-simple-span.sh",
    "content": "#!/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 be useful\nst=$(date +%s.%N) # unix epoch time with nanoseconds\ndata1=$(uuidgen)\ndata2=$(uuidgen)\net=$(date +%s.%N)\n\n# don't worry, there are also short options :)\n../otel-cli span \\\n\t--service  \"otel-cli-demo\"    \\\n\t--name     \"hello world\" \\\n\t--kind     \"client\"      \\\n\t--start    $st           \\\n\t--end      $et           \\\n\t--tp-print               \\\n\t--attrs \"my.data1=$data1,my.data2=$data2\"\n"
  },
  {
    "path": "demos/05-nested-exec.sh",
    "content": "#!/bin/bash\n# an otel-cli demo of nested exec\n#\n# this isn't necessarily practical, but it demonstrates how the TRACEPARENT\n# environment variable carries the context from invocation to invocation\n# so that the tracing provider (e.g. Honeycomb) can put it all back together\n\n# set the service name automatically on calls to otel-cli\nexport OTEL_SERVICE_NAME=\"otel-cli-demo\"\n\n# generate a new trace & span, cli will print out the 'export TRACEPARENT'\ncarrier=$(mktemp)\n../otel-cli span -n \"traceparent demo $0\" --tp-print --tp-carrier $carrier\n\n# this will start a child span, and run another otel-cli as its program\n../otel-cli exec \\\n\t--name       \"hammer the server for sweet sweet data\" \\\n\t--kind       \"client\" \\\n\t--tp-carrier $carrier \\\n\t--verbose \\\n     \t--fail \\\n\t-- \\\n\t../otel-cli exec -n fake-server -k server /bin/echo 500 NOPE\n\t# ^ child span, the responding \"server\" that just echos NOPE\n\n"
  },
  {
    "path": "demos/10-span-background-simple.sh",
    "content": "#!/bin/bash\n# an otel-cli demo of span background\n\n../otel-cli span background \\\n    --service otel-cli-demo \\\n    --name \"executing $0\" \\\n    --timeout 2 &\n#               ^ run otel-cli in the background\nsleep 1\n\n# that's it, that's the demo\n# when this script exits, otel-cli will exit too so total runtime will\n# be a bit over 1 second\n"
  },
  {
    "path": "demos/15span-background-layered.sh",
    "content": "#!/bin/bash\n# an otel-cli demo of span background\n#\n# This demo shows span background functionality with events added to the span\n# while it's running in the background, then a child span is created and\n# the background span is ended gracefully.\n\nset -e\nset -x\n\ncarrier=$(mktemp)    # traceparent propagation via tempfile\nsockdir=$(mktemp -d) # a unix socket will be created here\n\nexport OTEL_SERVICE_NAME=\"otel-cli-demo\"\n\n# start the span background server, set up trace propagation, and\n# time out after 10 seconds (which shouldn't be reached)\n../otel-cli span background \\\n    --verbose --fail \\\n    --tp-carrier $carrier \\\n    --sockdir $sockdir \\\n    --tp-print \\\n    --name \"$0 script execution\" \\\n    --timeout 10 &\n\ndata1=$(uuidgen)\n\n# add an event to the span running in the background, with an attribute\n# set to the uuid we just generated\n../otel-cli span event \\\n    --verbose \\\n    --name \"did a thing\" \\\n    --sockdir $sockdir \\\n    --attrs \"data1=$data1\"\n\n# waste some time\nsleep 1\n\n# add an event that says we wasted some time\n../otel-cli span event --verbose --name \"slept 1 second\" --sockdir $sockdir\n\n# run a shorter sleep inside a child span, also note that this is using\n# --tp-required so this will fail loudly if there is no traceparent\n# available\n../otel-cli exec \\\n    --verbose --fail \\\n    --name \"sleep 0.2\" \\\n    --tp-required \\\n    --tp-carrier $carrier \\\n    --tp-print \\\n    sleep 0.2\n\n# finally, tell the background server we're all done and it can exit\n../otel-cli span end --sockdir $sockdir\n\n"
  },
  {
    "path": "demos/20span-background-race-workarounds.sh",
    "content": "#!/bin/bash\n# an otel-cli demo of workarounds for race conditions on span background\n#\n# otel-cli span background is usually run as a subprocess with & in the command\n# as below. An issue that shows up sometimes is a race condition where the shell\n# starts otel-cli in the background, and then immediately calls otel-cli span\n# or similar hoping to use the --tp-carrier file, which might not be created\n# before the process looks for it. There are a couple solutions below.\n\nset -e\nset -x\n\ncarrier=$(mktemp)    # traceparent propagation via tempfile\nsockdir=$(mktemp -d) # a unix socket will be created here\n\nexport OTEL_SERVICE_NAME=\"otel-cli-demo\"\n\n../otel-cli span background \\\n    --tp-carrier $carrier \\\n    --sockdir $sockdir \\\n    --service otel-cli \\\n    --name \"$0 script execution #1\" \\\n    --timeout 10 &\n\n# On Linux, the inotifywait command will do the trick, waiting for the\n# file to be written. Without a timeout it could hang forever if it loses\n# the race and otel-cli finishes writing the carrier before inotifywait\n# starts watching. A short timeout ensures it won't hang.\n[ ! -e $carrier ] && inotifywait --timeout 0.1 $carrier\n../otel-cli span --tp-carrier $carrier --name \"child of span background, after inotifywait\"\n../otel-cli span end --sockdir $sockdir\n\n# start another one for the second example\n../otel-cli span background \\\n    --tp-carrier $carrier \\\n    --sockdir $sockdir \\\n    --service otel-cli \\\n    --name \"$0 script execution #2\" \\\n    --timeout 10 &\n\n# otel-cli span event already waits for span background's socket file\n# to appear so just sending an event will do enough synchronization, at\n# the cost of a meaningless event.\n../otel-cli span event --sockdir $sockdir --name \"wait for span background\"\n../otel-cli span --tp-carrier $carrier --name \"child of span background, after span event\"\n../otel-cli span end --sockdir $sockdir\n"
  },
  {
    "path": "demos/25srecon22-talk-agenda.sh",
    "content": "#!/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 for this exercise since\n# we don't show it\n# but it is important for /finding/ these spans once you've sent them\n# so use today's date and hour but munge the rest\ntalk_start=$(date +%Y-%m-%dT%H:00:00.00000Z)\ntalk_end=$(date +%Y-%m-%dT%H:00:40.00000Z)\n\ntpfile=$(mktemp)\notel-cli span \\\n\t--name \"OpenTelemetry::Trace.tracer(BareMetal)\" \\\n\t--service \"$svc\" \\\n\t--start \"$talk_start\" \\\n\t--end \"$talk_end\" \\\n\t--tp-print \\\n\t--tp-export \\\n\t--tp-carrier $tpfile\n\n# set the traceparent\n. $tpfile\n\notel-cli span \\\n\t--name \"Agenda\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:01:00/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:02:00/\")\"\n\nsubtpfile=$(mktemp)\notel-cli span \\\n\t--name \"The Bad Old Days\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:02:01/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:03:01/\")\"\n\nsubtpfile=$(mktemp)\notel-cli span \\\n\t--name \"Tracing All the Things!\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:03:01/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:10:01/\")\" \\\n\t--tp-export \\\n\t--tp-carrier $subtpfile\n. $subtpfile # next spans are under ^^\n\totel-cli span \\\n\t\t--name \"OTel in the Metal API\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:03:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:04:00/\")\"\n\totel-cli span \\\n\t\t--name \"Fits and Starts\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:04:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:07:00/\")\"\n\totel-cli span \\\n\t\t--name \"Introducing the Metal SRE team\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:07:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:10:00/\")\"\n\n. $tpfile # back to top trace\n\nsubtpfile=$(mktemp)\notel-cli span \\\n\t--name \"Observability Onboarding\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:10:01/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:15:01/\")\" \\\n\t--tp-export \\\n\t--tp-carrier $subtpfile\n. $subtpfile # next spans are under ^^\n\totel-cli span \\\n\t\t--name \"Order of Operations\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:10:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:12:00/\")\"\n\totel-cli span \\\n\t\t--name \"Mental Models\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:12:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:14:00/\")\"\n\totel-cli span \\\n\t\t--name \"How We Know We're on the Right Track\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:14:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:16:00/\")\"\n\n. $tpfile # back to top trace\n\nsubtpfile=$(mktemp)\notel-cli span \\\n\t--name \"Tracing Wins\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:16:01/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:22:01/\")\" \\\n\t--tp-export \\\n\t--tp-carrier $subtpfile\n. $subtpfile # next spans are under ^^\n\totel-cli span \\\n\t\t--name \"Performance Project\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:16:02/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:19:00/\")\"\n\totel-cli span \\\n\t\t--name \"Sociotechnical Wins\" \\\n\t\t--service \"$svc\" \\\n\t\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:19:01/\")\" \\\n\t\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:22:00/\")\"\n\n. $tpfile # back to top trace\n\notel-cli span \\\n\t--name \"Tracing Bear Metal\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:22:01/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:28:00/\")\"\n\notel-cli span \\\n\t--name \"Recap\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:28:01/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:29:59/\")\"\n\notel-cli span \\\n\t--name \"Q & A\" \\\n\t--service \"$svc\" \\\n\t--start \"$(echo \"$talk_start\" |sed \"s/:00:00/:30:00/\")\" \\\n\t--end   \"$(echo \"$talk_start\" |sed \"s/:00:00/:40:00/\")\"\n"
  },
  {
    "path": "demos/30-trace-build-process/otel-wrapper-shim.sh",
    "content": "#!/bin/bash\n\n# Build Process Tracing Example\n#\n# It's possible to instrument complex build processes in nodejs (npm/yarn/pnpm) and\n# C (make/cmake/ninja) by injecting instrumented versions of some key commands into $PATH.\n#\n# This example is a folder with a shim script in it, and a bunch of fake tools that are just\n# symlinks to the shim. It needs to be a folder so that it can be cleanly injected into $PATH.\n#\n# To see this working, start `otel-desktop-viewer` as discribed in the main README.md, and then:\n# (\n#   cd .. && \\\n#   ([ -d mermaid ] || git clone https://github.com/mermaid-js/mermaid) && \\\n#   cd mermaid/ && \\\n#   ../otel-cli/demos/30-trace-build-process/pnpm install\n# )\n#\n# For more complex build processes, I have found jaeger to be quite good, because it also includes\n# a black \"critical path line\". See https://www.jaegertracing.io/docs/1.54/getting-started/ for\n# their all-in-one docker conainer setup instructions. Otherwise, everything else is the same as\n# when using otel-desktop-viewer.\n\nset -euo pipefail\n\nexport OTEL_EXPORTER_OTLP_ENDPOINT=\"${OTEL_EXPORTER_OTLP_ENDPOINT:-localhost:4317}\"\n\nTOOL_NAME=\"$(basename $0)\"\nLOCATION_IN_PATH=\"$(dirname $0)\"\nHERE=\"$(dirname $(readlink -f $0))\"\n# This is just a guess, based on what's left after we've removed ourselves and any symlinks we know about from the results.\n# If we're symlinked into $PATH in more than once place then we could still end up in loops.\n# If this becomes a problem, we could instead iterate over each location and call readlink,\n# and drop anything that resolves to $HERE.\nORIGINAL_TOOL_PATH=\"$(which -a \"$TOOL_NAME\" | grep -Ev \"($HERE|$LOCATION_IN_PATH)\" | head -n1)\"\n\n# Put this dir first in $PATH so that nested calls out to bash and pnpm are instrumented\n# (tools like npm have a habit of messing with $PATH to put themselves first in subshells).\n# This will probably get quite long if there is a lot of recursion.\n# If this causes problems, we could prune ouselves out before inserting ourself at the head.\nexport PATH=\"$HERE:$PATH\"\n\notel-cli exec --service \"$TOOL_NAME\" --name \"$*\" -- $ORIGINAL_TOOL_PATH \"$@\"\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "---\nversion: '2.1'\nservices:\n  jaeger:\n    image: jaegertracing/all-in-one:1.58.0\n    ports:\n      - \"16686:16686\"\n      - \"14268\"\n      - \"14250\"\n  otel-collector:\n    image: otel/opentelemetry-collector:0.76.1\n    volumes:\n      - ./configs/otel-collector.yaml:/local.yaml\n    command: --config /local.yaml\n    ports:\n      - \"4317:4317\"\n    depends_on:\n      - jaeger\n"
  },
  {
    "path": "example-config.json",
    "content": "{\n   \"endpoint\" : \"localhost:4317\",\n   \"traces_endpoint\": \"\",\n   \"timeout\" : \"1s\",\n   \"otlp_headers\" : {\n      \"header1\" : \"header1-value\"\n   },\n   \"otlp_blocking\" : false,\n\n   \"insecure\" : true,\n   \"tls_no_verify\" : true,\n   \"tls_ca_cert\": \"/dev/null\",\n   \"tls_client_key\": \"/dev/null\",\n   \"tls_client_cert\": \"/dev/null\",\n\n   \"service_name\" : \"configured_in_config_file\",\n\n   \"span_name\" : \"config_file_span\",\n   \"span_kind\" : \"server\",\n   \"span_attributes\" : {\n      \"attr1\" : \"value1\"\n   },\n   \"span_end_time\" : \"now\",\n   \"span_start_time\" : \"now\",\n   \"span_status_code\" : \"0\",\n   \"span_status_description\" : \"status description\",\n\n   \"event_name\" : \"config_file_event\",\n   \"event_time\" : \"now\",\n\n   \"background_parent_poll_ms\" : 100,\n   \"background_socket_directory\" : \"/tmp\",\n   \"background_wait\" : true,\n\n   \"traceparent_carrier_file\" : \"/tmp/traceparent.txt\",\n   \"traceparent_ignore_env\" : true,\n   \"traceparent_print\" : true,\n   \"traceparent_print_export\" : true,\n   \"traceparent_required\" : false,\n\n   \"verbose\" : true,\n   \"fail\" : true\n}\n"
  },
  {
    "path": "go.mod",
    "content": "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.com/pterm/pterm v0.12.79\n\tgithub.com/spf13/cobra v1.8.0\n\tgo.opentelemetry.io/otel v1.27.0\n\tgo.opentelemetry.io/otel/sdk v1.27.0\n\tgo.opentelemetry.io/proto/otlp v1.1.0\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3\n\tgoogle.golang.org/grpc v1.64.0\n\tgoogle.golang.org/protobuf v1.34.2\n)\n\nrequire (\n\tatomicgo.dev/cursor v0.2.0 // indirect\n\tatomicgo.dev/keyboard v0.2.9 // indirect\n\tatomicgo.dev/schedule v0.1.0 // indirect\n\tgithub.com/containerd/console v1.0.3 // indirect\n\tgithub.com/go-logr/logr v1.4.1 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/gookit/color v1.5.4 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/lithammer/fuzzysearch v1.1.8 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.15 // indirect\n\tgithub.com/rivo/uniseg v0.4.4 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgo.opentelemetry.io/otel/metric v1.27.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.27.0 // indirect\n\tgolang.org/x/net v0.22.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/term v0.18.0 // indirect\n\tgolang.org/x/text v0.14.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=\natomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=\natomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=\natomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=\natomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=\natomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=\natomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=\natomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=\ngithub.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=\ngithub.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=\ngithub.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=\ngithub.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=\ngithub.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=\ngithub.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=\ngithub.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=\ngithub.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=\ngithub.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=\ngithub.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=\ngithub.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=\ngithub.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=\ngithub.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=\ngithub.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=\ngithub.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=\ngithub.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=\ngithub.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=\ngithub.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=\ngithub.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=\ngithub.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=\ngithub.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=\ngithub.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=\ngithub.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=\ngithub.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=\ngithub.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=\ngithub.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4=\ngithub.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=\ngithub.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=\ngo.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=\ngo.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=\ngo.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=\ngo.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=\ngo.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=\ngo.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=\ngo.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=\ngo.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=\ngo.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=\ngo.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=\ngo.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=\ngo.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=\ngo.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=\ngo.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=\ngo.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=\ngo.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=\ngo.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=\ngolang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=\ngolang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=\ngolang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=\ngolang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=\ngolang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=\ngolang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=\ngolang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=\ngoogle.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 h1:9Xyg6I9IWQZhRVfCWjKK+l6kI0jHcPesVlMnT//aHNo=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=\ngoogle.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=\ngoogle.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=\ngoogle.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=\ngoogle.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "main.go",
    "content": "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 & ldflags at build time\nvar (\n\tversion = \"\"\n\tcommit  = \"\"\n\tdate    = \"\"\n)\n\nfunc main() {\n\totelcli.Execute(otelcli.FormatVersion(version, commit, date))\n\tos.Exit(otelcli.GetExitCode())\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "package main_test\n\n// This file implements the data-driven test harness for otel-cli. It executes\n// tests defined in data_for_test.go, and uses the CA implementation in\n// tls_for_test.go.\n//\n// see TESTING.md for details\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpclient\"\n\t\"github.com/equinix-labs/otel-cli/otlpserver\"\n\t\"github.com/google/go-cmp/cmp\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// otel-cli will fail with \"getent not found\" if PATH is empty\n// so set it to the bare minimum and always the same for cleanup\nconst minimumPath = `/bin:/usr/bin`\nconst defaultTestTimeout = time.Second\n\nfunc TestMain(m *testing.M) {\n\t// wipe out this process's envvars right away to avoid pollution & leakage\n\tos.Clearenv()\n\tresult := m.Run()\n\tos.Exit(result)\n}\n\n// TestOtelCli iterates over all defined fixtures and executes the tests.\nfunc TestOtelCli(t *testing.T) {\n\t_, err := os.Stat(\"./otel-cli\")\n\tif os.IsNotExist(err) {\n\t\tt.Fatalf(\"otel-cli must be built and present as ./otel-cli for this test suite to work (try: go build)\")\n\t}\n\n\tvar fixtureCount int\n\tfor _, suite := range suites {\n\t\tfixtureCount += len(suite)\n\t\tfor i, fixture := range suite {\n\t\t\t// clean up some nils here so the test data can be a bit more terse\n\t\t\tif fixture.Config.CliArgs == nil {\n\t\t\t\tsuite[i].Config.CliArgs = []string{}\n\t\t\t}\n\t\t\tif fixture.Config.Env == nil {\n\t\t\t\tsuite[i].Config.Env = map[string]string{}\n\t\t\t}\n\t\t\tif fixture.Expect.Env == nil {\n\t\t\t\tsuite[i].Expect.Env = map[string]string{}\n\t\t\t}\n\t\t\tif fixture.Expect.SpanData == nil {\n\t\t\t\tsuite[i].Expect.SpanData = map[string]string{}\n\t\t\t}\n\t\t\t// make sure PATH hasn't been set, because doing that in fixtures is naughty\n\t\t\tif _, ok := fixture.Config.Env[\"PATH\"]; ok {\n\t\t\t\tt.Fatalf(\"fixture in file %s is not allowed to modify or test envvar PATH\", fixture.Name)\n\t\t\t}\n\t\t}\n\t}\n\n\tt.Logf(\"Running %d test suites and %d fixtures.\", len(suites), fixtureCount)\n\tif len(suites) == 0 || fixtureCount == 0 {\n\t\tt.Fatal(\"no test fixtures loaded!\")\n\t}\n\n\t// generates a CA, client, and server certs to use in tests\n\ttlsData := generateTLSData(t)\n\tdefer tlsData.cleanup()\n\n\tfor _, suite := range suites {\n\t\t// a fixture can be backgrounded after starting it up for e.g. otel-cli span background\n\t\t// a second fixture with the same description later in the list will \"foreground\" it\n\t\tbgFixtureWaits := make(map[string]chan struct{})\n\t\tbgFixtureDones := make(map[string]chan struct{})\n\n\tfixtures:\n\t\tfor _, fixture := range suite {\n\t\t\t// some tests explicitly spend time sleeping/waiting to validate timeouts are working\n\t\t\t// and when they are marked as such, they can be skipped with go test -test.short\n\t\t\tif testing.Short() && fixture.Config.IsLongTest {\n\t\t\t\tt.Skipf(\"[%s] skipping timeout tests in short mode\", fixture.Name)\n\t\t\t\tcontinue fixtures\n\t\t\t}\n\n\t\t\t// inject the TlsData into the fixture\n\t\t\tfixture.TlsData = tlsData\n\n\t\t\t// when a fixture is foregrounded all it does is signal the background fixture\n\t\t\t// to finish doing its then, waits for it to finish, then continues on\n\t\t\tif fixture.Config.Foreground {\n\t\t\t\tif wait, ok := bgFixtureWaits[fixture.Name]; ok {\n\t\t\t\t\twait <- struct{}{}\n\t\t\t\t\tdelete(bgFixtureWaits, fixture.Name)\n\t\t\t\t} else {\n\t\t\t\t\tt.Fatalf(\"BUG in test or fixture: unexpected foreground fixture wait chan named %q\", fixture.Name)\n\t\t\t\t}\n\t\t\t\tif done, ok := bgFixtureDones[fixture.Name]; ok {\n\t\t\t\t\t<-done\n\t\t\t\t\tdelete(bgFixtureDones, fixture.Name)\n\t\t\t\t} else {\n\t\t\t\t\tt.Fatalf(\"BUG in test or fixture: unexpected foreground fixture done chan named %q\", fixture.Name)\n\t\t\t\t}\n\n\t\t\t\tcontinue fixtures\n\t\t\t}\n\n\t\t\t// flow control for backgrounding fixtures:\n\t\t\tfixtureWait := make(chan struct{})\n\t\t\tfixtureDone := make(chan struct{})\n\n\t\t\tgo runFixture(t, fixture, fixtureWait, fixtureDone)\n\n\t\t\tif fixture.Config.Background {\n\t\t\t\t// save off the channels for flow control\n\t\t\t\tt.Logf(\"[%s] fixture %q backgrounded\", fixture.Name, fixture.Name)\n\t\t\t\tbgFixtureWaits[fixture.Name] = fixtureWait\n\t\t\t\tbgFixtureDones[fixture.Name] = fixtureDone\n\t\t\t} else {\n\t\t\t\t// actually the default case, just block as if the code was ran synchronously\n\t\t\t\tfixtureWait <- struct{}{}\n\t\t\t\t<-fixtureDone\n\t\t\t}\n\t\t}\n\t}\n}\n\n// runFixture runs the OTLP server & command, waits for signal, checks\n// results, then signals it's done.\nfunc runFixture(t *testing.T, fixture Fixture, wait, done chan struct{}) {\n\t// sets up an OTLP server, runs otel-cli, packages data up in these return vals\n\tendpoint, results := runOtelCli(t, fixture)\n\t<-wait\n\n\t// inject the runtime endpoint into the fixture\n\tfixture.Endpoint = endpoint\n\tcheckAll(t, fixture, results)\n\tdone <- struct{}{}\n}\n\n// checkAll gathers up all the check* funcs below into one function.\nfunc checkAll(t *testing.T, fixture Fixture, results Results) {\n\t// check timeout and process status expectations\n\tsuccess := checkProcess(t, fixture, results)\n\t// when the process fails, no point in checking the rest, it's just noise\n\tif !success {\n\t\tt.Log(\"otel-cli process failed unexpectedly, will not test values from it\")\n\t\treturn\n\t}\n\n\t// compare the number of recorded spans against expectations in the fixture\n\tcheckSpanCount(t, fixture, results)\n\n\t// compares the data in each recorded span against expectations in the fixture\n\tif len(fixture.Expect.SpanData) > 0 {\n\t\tcheckSpanData(t, fixture, results)\n\t}\n\n\t// many of the basic plumbing tests use status so it has its own set of checks\n\t// but these shouldn't run for testing the other subcommands\n\tif len(fixture.Config.CliArgs) > 0 && fixture.Config.CliArgs[0] == \"status\" && results.ExitCode == 0 {\n\t\tcheckStatusData(t, fixture, results)\n\t} else {\n\t\t// checking the text output only makes sense for non-status paths\n\t\tcheckOutput(t, fixture, results)\n\t}\n\n\tif len(fixture.Expect.Headers) > 0 {\n\t\tcheckHeaders(t, fixture, results)\n\t}\n\n\tif len(fixture.Expect.ServerMeta) > 0 {\n\t\tcheckServerMeta(t, fixture, results)\n\t}\n\n\tcheckFuncs(t, fixture, results)\n}\n\n// compare the number of spans recorded by the test server against the\n// number of expected spans in the fixture, if specified. If no expected\n// span count is specified, this check always passes.\nfunc checkSpanCount(t *testing.T, fixture Fixture, results Results) {\n\tif results.SpanCount != fixture.Expect.SpanCount {\n\t\tt.Errorf(\"[%s] span count was %d but expected %d\", fixture.Name, results.SpanCount, fixture.Expect.SpanCount)\n\t}\n}\n\n// checkProcess validates configured expectations about whether otel-cli failed\n// or the test timed out. These are mostly used for testing that otel-cli fails\n// in the way we want it to.\nfunc checkProcess(t *testing.T, fixture Fixture, results Results) bool {\n\tif results.TimedOut != fixture.Expect.TimedOut {\n\t\tt.Errorf(\"[%s] test timeout status is %t but expected %t\", fixture.Name, results.TimedOut, fixture.Expect.TimedOut)\n\t\treturn false\n\t}\n\tif results.CommandFailed != fixture.Expect.CommandFailed {\n\t\tt.Errorf(\"[%s] command failed is %t but expected %t\", fixture.Name, results.CommandFailed, fixture.Expect.CommandFailed)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// checkOutput looks that otel-cli output stored in the results and compares against\n// the fixture expectation (with {{endpoint}} replaced).\nfunc checkOutput(t *testing.T, fixture Fixture, results Results) {\n\twantOutput := injectVars(fixture.Expect.CliOutput, fixture.Endpoint, fixture.TlsData)\n\tgotOutput := results.CliOutput\n\tif fixture.Expect.CliOutputRe != nil {\n\t\tgotOutput = fixture.Expect.CliOutputRe.ReplaceAllString(gotOutput, \"\")\n\t}\n\tif diff := cmp.Diff(wantOutput, gotOutput); diff != \"\" {\n\t\tif fixture.Expect.CliOutputRe != nil {\n\t\t\tt.Errorf(\"[%s] test fixture RE modified output from %q to %q\", fixture.Name, fixture.Expect.CliOutput, gotOutput)\n\t\t}\n\t\tt.Errorf(\"[%s] otel-cli output did not match fixture (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n}\n\n// checkStatusData compares the sections of otel-cli status output against\n// fixture data, substituting {{endpoint}} into fixture data as needed.\nfunc checkStatusData(t *testing.T, fixture Fixture, results Results) {\n\t// check the env\n\tinjectMapVars(fixture.Endpoint, fixture.Expect.Env, fixture.TlsData)\n\tif diff := cmp.Diff(fixture.Expect.Env, results.Env); diff != \"\" {\n\t\tt.Errorf(\"env data did not match fixture in %q (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n\n\t// check diagnostics, use string maps so the diff output is easy to compare to json\n\twantDiag := fixture.Expect.Diagnostics.ToStringMap()\n\tgotDiag := results.Diagnostics.ToStringMap()\n\tinjectMapVars(fixture.Endpoint, wantDiag, fixture.TlsData)\n\t// there's almost always going to be cli_args in practice, so if the fixture has\n\t// an empty string, just ignore that key\n\tif wantDiag[\"cli_args\"] == \"\" {\n\t\tgotDiag[\"cli_args\"] = \"\"\n\t}\n\tfor k, v := range wantDiag {\n\t\tif v == \"*\" {\n\t\t\twantDiag[k] = gotDiag[k]\n\t\t}\n\t}\n\tif diff := cmp.Diff(wantDiag, gotDiag); diff != \"\" {\n\t\tt.Errorf(\"[%s] diagnostic data did not match fixture (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n\n\t// check the configuration\n\twantConf := fixture.Expect.Config.ToStringMap()\n\tgotConf := results.Config.ToStringMap()\n\t// if an expected config string is set to \"*\" it will match anything\n\t// and is effectively ignored\n\tfor k, v := range wantConf {\n\t\tif v == \"*\" {\n\t\t\t// set to same so cmd.Diff will ignore\n\t\t\twantConf[k] = gotConf[k]\n\t\t}\n\t}\n\tinjectMapVars(fixture.Endpoint, wantConf, fixture.TlsData)\n\tif diff := cmp.Diff(wantConf, gotConf); diff != \"\" {\n\t\tt.Errorf(\"[%s] config data did not match fixture (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n}\n\n// checkSpanData compares the data in spans received from otel-cli against the\n// fixture data.\nfunc checkSpanData(t *testing.T, fixture Fixture, results Results) {\n\t// check the expected span data against what was received by the OTLP server\n\tgotSpan := otlpclient.SpanToStringMap(results.Span, results.ResourceSpans)\n\tinjectMapVars(fixture.Endpoint, gotSpan, fixture.TlsData)\n\twantSpan := map[string]string{} // to be passed to cmp.Diff\n\n\t// verify all fields that were expected were present in output span\n\tfor what := range fixture.Expect.SpanData {\n\t\tif _, ok := gotSpan[what]; !ok {\n\t\t\tt.Errorf(\"[%s] expected span to have key %q but it is not present\", fixture.Name, what)\n\t\t}\n\t}\n\n\t// spanRegexChecks is a map of field names and regexes for basic presence\n\t// and validity checking of span data fields with expected set to \"*\"\n\tspanRegexChecks := map[string]*regexp.Regexp{\n\t\t\"trace_id\":   regexp.MustCompile(`^[0-9a-fA-F]{32}$`),\n\t\t\"span_id\":    regexp.MustCompile(`^[0-9a-fA-F]{16}$`),\n\t\t\"name\":       regexp.MustCompile(`^\\w+$`),\n\t\t\"parent\":     regexp.MustCompile(`^[0-9a-fA-F]{32}$`),\n\t\t\"kind\":       regexp.MustCompile(`^\\w+$`), // TODO: can validate more here\n\t\t\"start\":      regexp.MustCompile(`^\\d+$`),\n\t\t\"end\":        regexp.MustCompile(`^\\d+$`),\n\t\t\"attributes\": regexp.MustCompile(`\\w+=.+`), // not great but should validate at least one k=v\n\t}\n\n\t// go over all the keys in the received span and check against expected values\n\t// in the fixture, and the spanRegexChecks\n\t// modifies the gotSpan/wantSpan maps so cmp.Diff can do its thing\n\tfor what, gotVal := range gotSpan {\n\t\tvar wantVal string\n\t\tvar ok bool\n\t\tif wantVal, ok = fixture.Expect.SpanData[what]; ok {\n\t\t\twantSpan[what] = wantVal\n\t\t} else {\n\t\t\twantSpan[what] = gotVal // make a straight copy to make cmp.Diff happy\n\t\t}\n\n\t\tif re, ok := spanRegexChecks[what]; ok {\n\t\t\tif wantVal == \"*\" {\n\t\t\t\t// * means if the above RE returns cleanly then pass the test\n\t\t\t\tif re.MatchString(gotVal) {\n\t\t\t\t\tdelete(gotSpan, what)\n\t\t\t\t\tdelete(wantSpan, what)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"[%s] span value %q for key %s is not valid\", fixture.Name, gotVal, what)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tinjectMapVars(fixture.Endpoint, wantSpan, fixture.TlsData)\n\n\t// a regular expression can be put in e.g. /^foo$/ to get evaluated as RE\n\tfor key, wantVal := range wantSpan {\n\t\tif strings.HasPrefix(wantVal, \"/\") && strings.HasSuffix(wantVal, \"/\") {\n\t\t\tre := regexp.MustCompile(wantVal[1 : len(wantVal)-1]) // slice strips the /'s off\n\t\t\tif !re.MatchString(gotSpan[key]) {\n\t\t\t\tt.Errorf(\"regular expression %q did not match on %q\", wantVal, gotSpan[key])\n\t\t\t}\n\t\t\tdelete(gotSpan, key) // delete from both maps so cmp.Diff ignores them\n\t\t\tdelete(wantSpan, key)\n\t\t}\n\t}\n\n\t// do a diff on a generated map that sets values to * when the * check succeeded\n\tif diff := cmp.Diff(wantSpan, gotSpan); diff != \"\" {\n\t\tt.Errorf(\"[%s] otel span info did not match fixture (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n}\n\n// checkHeaders compares the expected and received headers.\nfunc checkHeaders(t *testing.T, fixture Fixture, results Results) {\n\t// gzip encoding makes automatically comparing values tricky, so ignore it\n\t// unless the test actually specifies a length\n\tif _, ok := fixture.Expect.Headers[\"Content-Length\"]; !ok {\n\t\tdelete(results.Headers, \"Content-Length\")\n\t}\n\n\tinjectMapVars(fixture.Endpoint, fixture.Expect.Headers, fixture.TlsData)\n\tinjectMapVars(fixture.Endpoint, results.Headers, fixture.TlsData)\n\n\tfor k, v := range fixture.Expect.Headers {\n\t\tif v == \"*\" {\n\t\t\t// overwrite value so cmp.Diff will ignore\n\t\t\tresults.Headers[k] = \"*\"\n\t\t}\n\t}\n\n\tif diff := cmp.Diff(fixture.Expect.Headers, results.Headers); diff != \"\" {\n\t\tt.Errorf(\"[%s] headers did not match expected (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n}\n\n// checkServerMeta compares the expected and received server metadata.\nfunc checkServerMeta(t *testing.T, fixture Fixture, results Results) {\n\tinjectMapVars(fixture.Endpoint, fixture.Expect.ServerMeta, fixture.TlsData)\n\tinjectMapVars(fixture.Endpoint, results.ServerMeta, fixture.TlsData)\n\n\tif diff := cmp.Diff(fixture.Expect.ServerMeta, results.ServerMeta); diff != \"\" {\n\t\tt.Errorf(\"[%s] server metadata did not match expected (-want +got):\\n%s\", fixture.Name, diff)\n\t}\n}\n\n// checkFuncs runs through the list of funcs in the fixture to do\n// complex checking on data. Funcs should call t.Errorf, etc. as needed.\nfunc checkFuncs(t *testing.T, fixture Fixture, results Results) {\n\tfor _, f := range fixture.CheckFuncs {\n\t\tf(t, fixture, results)\n\t}\n}\n\n// runOtelCli runs the a server and otel-cli together and captures their\n// output as data to return for further testing.\nfunc runOtelCli(t *testing.T, fixture Fixture) (string, Results) {\n\tstarted := time.Now()\n\n\tresults := Results{\n\t\tSpanData:   map[string]string{},\n\t\tEnv:        map[string]string{},\n\t\tSpanEvents: []*tracepb.Span_Event{},\n\t}\n\n\t// these channels need to be buffered or the callback will hang trying to send\n\trcvSpan := make(chan *tracepb.Span, 100) // 100 spans is enough for anybody\n\trcvEvents := make(chan []*tracepb.Span_Event, 100)\n\n\t// otlpserver calls this function for each span received\n\tcb := func(ctx context.Context, span *tracepb.Span, events []*tracepb.Span_Event, rss *tracepb.ResourceSpans, headers map[string]string, meta map[string]string) bool {\n\t\trcvSpan <- span\n\t\trcvEvents <- events\n\n\t\tresults.ServerMeta = meta\n\t\tresults.ResourceSpans = rss\n\t\tresults.SpanCount++\n\t\tresults.EventCount += len(events)\n\t\tresults.Headers = headers\n\n\t\t// true tells the server we're done and it can exit its loop\n\t\treturn results.SpanCount >= fixture.Expect.SpanCount\n\t}\n\n\tvar cs otlpserver.OtlpServer\n\tswitch fixture.Config.ServerProtocol {\n\tcase grpcProtocol:\n\t\tcs = otlpserver.NewServer(\"grpc\", cb, func(otlpserver.OtlpServer) {})\n\tcase httpProtocol:\n\t\tcs = otlpserver.NewServer(\"http\", cb, func(otlpserver.OtlpServer) {})\n\t}\n\tdefer cs.Stop()\n\n\tserverTimeout := time.Duration(fixture.Config.TestTimeoutMs) * time.Millisecond\n\tif serverTimeout == time.Duration(0) {\n\t\tserverTimeout = defaultTestTimeout\n\t}\n\n\t// start a timeout goroutine for the otlp server, cancelable with done<-struct{}{}\n\tcancelServerTimeout := make(chan struct{}, 1)\n\tgo func() {\n\t\tselect {\n\t\tcase <-time.After(serverTimeout):\n\t\t\tresults.TimedOut = true\n\t\t\tcs.Stop() // supports multiple calls\n\t\tcase <-cancelServerTimeout:\n\t\t\treturn\n\t\t}\n\t}()\n\n\t// port :0 means randomly assigned port, which we copy into {{endpoint}}\n\tvar listener net.Listener\n\tvar err error\n\tif fixture.Config.ServerTLSEnabled {\n\t\ttlsConf := fixture.TlsData.serverTLSConf.Clone()\n\t\tif fixture.Config.ServerTLSAuthEnabled {\n\t\t\ttlsConf.ClientAuth = tls.RequireAndVerifyClientCert\n\t\t}\n\t\tlistener, err = tls.Listen(\"tcp\", \"localhost:0\", tlsConf)\n\t} else {\n\t\tlistener, err = net.Listen(\"tcp\", \"localhost:0\")\n\t}\n\tendpoint := listener.Addr().String()\n\tif err != nil {\n\t\t// t.Fatalf is not allowed since we run this in a goroutine\n\t\tt.Errorf(\"[%s] failed to listen on OTLP endpoint %q: %s\", fixture.Name, endpoint, err)\n\t\treturn endpoint, results\n\t}\n\tt.Logf(\"[%s] starting OTLP server on %q\", fixture.Name, endpoint)\n\n\t// TODO: might be neat to have a mode where we start the listener and do nothing\n\t// with it to simulate a hung server or opentelemetry-collector\n\tgo func() {\n\t\tcs.Serve(listener)\n\t}()\n\n\t// let things go this far to generate the endpoint port then stop the server before\n\t// calling otel-cli so we can test timeouts\n\tif fixture.Config.StopServerBeforeExec {\n\t\tcs.Stop()\n\t\tlistener.Close()\n\t}\n\n\t// TODO: figure out the best way to build the binary and detect if the build is stale\n\t// ^^ probably doesn't matter much in CI, can auto-build, but for local workflow it matters\n\t// TODO: should all otel-cli commands be able to dump status? e.g. otel-cli span --status\n\targs := []string{}\n\tif len(fixture.Config.CliArgs) > 0 {\n\t\tfor _, v := range fixture.Config.CliArgs {\n\t\t\targs = append(args, injectVars(v, endpoint, fixture.TlsData))\n\t\t}\n\t}\n\tstatusCmd := exec.Command(\"./otel-cli\", args...)\n\tstatusCmd.Env = mkEnviron(endpoint, fixture.Config.Env, fixture.TlsData)\n\n\t// have command write output into string buffers\n\tvar cliOut bytes.Buffer\n\tstatusCmd.Stdout = &cliOut\n\tstatusCmd.Stderr = &cliOut\n\n\terr = statusCmd.Start()\n\tif err != nil {\n\t\tt.Fatalf(\"[%s] error starting otel-cli: %s\", fixture.Name, err)\n\t}\n\n\tstopKiller := make(chan struct{}, 1)\n\tif fixture.Config.KillAfter != 0 {\n\t\tgo func() {\n\t\t\tselect {\n\t\t\tcase <-time.After(fixture.Config.KillAfter):\n\t\t\t\terr := statusCmd.Process.Signal(fixture.Config.KillSignal)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"[%s] error sending signal %s to pid %d: %s\", fixture.Name, fixture.Config.KillSignal, statusCmd.Process.Pid, err)\n\t\t\t\t}\n\t\t\tcase <-stopKiller:\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\t} else {\n\t\tgo func() {\n\t\t\tselect {\n\t\t\tcase <-time.After(serverTimeout):\n\t\t\t\tt.Logf(\"[%s] timeout, killing process...\", fixture.Name)\n\t\t\t\tresults.TimedOut = true\n\t\t\t\terr = statusCmd.Process.Kill()\n\t\t\t\tif err != nil {\n\t\t\t\t\t// TODO: this might be a bit fragile, soften this up later if it ends up problematic\n\t\t\t\t\tlog.Fatalf(\"[%s] %d timeout process kill failed: %s\", fixture.Name, serverTimeout, err)\n\t\t\t\t}\n\t\t\tcase <-stopKiller:\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\t}\n\n\t// grab stderr & stdout comingled so that if otel-cli prints anything to either it's not\n\t// supposed to it will cause e.g. status json parsing and other tests to fail\n\tt.Logf(\"[%s] going to exec 'env -i %s %s'\", fixture.Name, strings.Join(statusCmd.Env, \" \"), strings.Join(statusCmd.Args, \" \"))\n\terr = statusCmd.Wait()\n\n\tresults.CliOutput = cliOut.String()\n\tresults.ExitCode = statusCmd.ProcessState.ExitCode()\n\tresults.CommandFailed = !statusCmd.ProcessState.Exited()\n\tif err != nil {\n\t\tt.Logf(\"[%s] command exited: %s\", fixture.Name, err)\n\t}\n\n\t// send stop signals to the timeouts and OTLP server\n\tcancelServerTimeout <- struct{}{}\n\tstopKiller <- struct{}{}\n\tcs.Stop()\n\n\t// only try to parse status json if it was a status command\n\t// TODO: support variations on otel-cli where status isn't the first arg\n\tif len(args) > 0 && args[0] == \"status\" && results.ExitCode == 0 {\n\t\terr = json.Unmarshal(cliOut.Bytes(), &results)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"[%s] parsing otel-cli status output failed: %s\", fixture.Name, err)\n\t\t\tt.Logf(\"[%s] output received: %q\", fixture.Name, cliOut)\n\t\t\treturn endpoint, results\n\t\t}\n\n\t\t// remove PATH from the output but only if it's exactly what we set on exec\n\t\tif path, ok := results.Env[\"PATH\"]; ok {\n\t\t\tif path == minimumPath {\n\t\t\t\tdelete(results.Env, \"PATH\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// when no spans are expected, return without reading from the channels\n\tif fixture.Expect.SpanCount == 0 {\n\t\treturn endpoint, results\n\t}\n\n\t// grab the spans & events from the server off the channels it writes to\n\tremainingTimeout := serverTimeout - time.Since(started)\n\tvar gatheredSpans int\ngather:\n\tfor {\n\t\tselect {\n\t\tcase <-time.After(remainingTimeout):\n\t\t\tbreak gather\n\t\tcase results.Span = <-rcvSpan:\n\t\t\t// events is always populated at the same time as the span is sent\n\t\t\t// and will always send at least an empty list\n\t\t\tresults.SpanEvents = <-rcvEvents\n\n\t\t\t// with this approach, any mismatch in spans produced and expected results\n\t\t\t// in a timeout with the above time.After\n\t\t\tgatheredSpans++\n\t\t\tif gatheredSpans == results.SpanCount {\n\t\t\t\t// TODO: it would be slightly nicer to use plural.Selectf instead of 'span(s)'\n\t\t\t\tt.Logf(\"[%s] test gathered %d span(s)\", fixture.Name, gatheredSpans)\n\t\t\t\tbreak gather\n\t\t\t}\n\t\t}\n\t}\n\n\treturn endpoint, results\n}\n\n// mkEnviron converts a string map to a list of k=v strings and tacks on PATH.\nfunc mkEnviron(endpoint string, env map[string]string, tlsData TlsSettings) []string {\n\tmapped := make([]string, len(env)+1)\n\tvar i int\n\tfor k, v := range env {\n\t\tmapped[i] = k + \"=\" + injectVars(v, endpoint, tlsData)\n\t\ti++\n\t}\n\n\t// always tack on a PATH otherwise the binary will fail with no PATH\n\t// to get to getent(1)\n\tmapped[len(mapped)-1] = \"PATH=\" + minimumPath\n\n\treturn mapped\n}\n\n// injectMapVars iterates over the map and updates the values, replacing all instances\n// of {{endpoint}}, {{tls_ca_cert}}, {{tls_client_cert}}, and {{tls_client_key}} with\n// test values.\nfunc injectMapVars(endpoint string, target map[string]string, tlsData TlsSettings) {\n\tfor k, v := range target {\n\t\ttarget[k] = injectVars(v, endpoint, tlsData)\n\t}\n}\n\n// injectVars replaces all instances of {{endpoint}}, {{tls_ca_cert}},\n// {{tls_client_cert}}, and {{tls_client_key}} with test values.\n// This is needed because the otlpserver is configured to listen on :0 which has it\n// grab a random port. Once that's generated we need to inject it into all the values\n// so the test comparisons work as expected. Similarly for TLS testing, a temp CA and\n// certs are created and need to be injected.\nfunc injectVars(in, endpoint string, tlsData TlsSettings) string {\n\tout := strings.ReplaceAll(in, \"{{endpoint}}\", endpoint)\n\tout = strings.ReplaceAll(out, \"{{tls_ca_cert}}\", tlsData.caFile)\n\tout = strings.ReplaceAll(out, \"{{tls_client_cert}}\", tlsData.clientFile)\n\tout = strings.ReplaceAll(out, \"{{tls_client_key}}\", tlsData.clientPrivKeyFile)\n\treturn out\n}\n"
  },
  {
    "path": "otelcli/completion.go",
    "content": "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 := cobra.Command{\n\t\tUse:   \"completion [bash|zsh|fish|powershell]\",\n\t\tShort: \"Generate completion script\",\n\t\tLong: `To load completions:\n\nBash:\n\n  $ source <(otel-cli completion bash)\n\n  # To load completions for each session, execute once:\n  # Linux:\n  $ otel-cli completion bash > /etc/bash_completion.d/otel-cli\n  # macOS:\n  $ otel-cli completion bash > /usr/local/etc/bash_completion.d/otel-cli\n\nZsh:\n\n  # If shell completion is not already enabled in your environment,\n  # you will need to enable it.  You can execute the following once:\n\n  $ echo \"autoload -U compinit; compinit\" >> ~/.zshrc\n\n  # To load completions for each session, execute once:\n  $ otel-cli completion zsh > \"${fpath[1]}/_otel-cli\"\n\n  # You will need to start a new shell for this setup to take effect.\n\nfish:\n\n  $ otel-cli completion fish | source\n\n  # To load completions for each session, execute once:\n  $ otel-cli completion fish > ~/.config/fish/completions/otel-cli.fish\n\nPowerShell:\n\n  PS> otel-cli completion powershell | Out-String | Invoke-Expression\n\n  # To load completions for every new session, run:\n  PS> otel-cli completion powershell > otel-cli.ps1\n  # and source this file from your PowerShell profile.\n`,\n\t\tDisableFlagsInUseLine: true,\n\t\tValidArgs:             []string{\"bash\", \"zsh\", \"fish\", \"powershell\"},\n\t\tArgs:                  cobra.MatchAll(cobra.ExactArgs(1)),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tswitch args[0] {\n\t\t\tcase \"bash\":\n\t\t\t\terr := cmd.Root().GenBashCompletion(os.Stdout)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"failed to write completion to stdout\")\n\t\t\t\t}\n\t\t\tcase \"zsh\":\n\t\t\t\terr := cmd.Root().GenZshCompletion(os.Stdout)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"failed to write completion to stdout\")\n\t\t\t\t}\n\t\t\tcase \"fish\":\n\t\t\t\terr := cmd.Root().GenFishCompletion(os.Stdout, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"failed to write completion to stdout\")\n\t\t\t\t}\n\t\t\tcase \"powershell\":\n\t\t\t\terr := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"failed to write completion to stdout\")\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "otelcli/config.go",
    "content": "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\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar detectBrokenRFC3339PrefixRe *regexp.Regexp\nvar epochNanoTimeRE *regexp.Regexp\n\nfunc init() {\n\tdetectBrokenRFC3339PrefixRe = regexp.MustCompile(`^\\d{4}-\\d{2}-\\d{2} `)\n\tepochNanoTimeRE = regexp.MustCompile(`^\\d+\\.\\d+$`)\n}\n\n// DefaultConfig returns a Config with all defaults set.\nfunc DefaultConfig() Config {\n\treturn Config{\n\t\tEndpoint:                     \"\",\n\t\tProtocol:                     \"\",\n\t\tTimeout:                      \"1s\",\n\t\tHeaders:                      map[string]string{},\n\t\tInsecure:                     false,\n\t\tBlocking:                     false,\n\t\tTlsNoVerify:                  false,\n\t\tTlsCACert:                    \"\",\n\t\tTlsClientKey:                 \"\",\n\t\tTlsClientCert:                \"\",\n\t\tServiceName:                  \"otel-cli\",\n\t\tSpanName:                     \"todo-generate-default-span-names\",\n\t\tKind:                         \"client\",\n\t\tForceTraceId:                 \"\",\n\t\tForceSpanId:                  \"\",\n\t\tForceParentSpanId:            \"\",\n\t\tAttributes:                   map[string]string{},\n\t\tTraceparentCarrierFile:       \"\",\n\t\tTraceparentIgnoreEnv:         false,\n\t\tTraceparentPrint:             false,\n\t\tTraceparentPrintExport:       false,\n\t\tTraceparentRequired:          false,\n\t\tBackgroundParentPollMs:       10,\n\t\tBackgroundSockdir:            \"\",\n\t\tBackgroundWait:               false,\n\t\tBackgroundSkipParentPidCheck: false,\n\t\tExecCommandTimeout:           \"\",\n\t\tExecTpDisableInject:          false,\n\t\tStatusCanaryCount:            1,\n\t\tStatusCanaryInterval:         \"\",\n\t\tSpanStartTime:                \"now\",\n\t\tSpanEndTime:                  \"now\",\n\t\tEventName:                    \"todo-generate-default-event-names\",\n\t\tEventTime:                    \"now\",\n\t\tCfgFile:                      \"\",\n\t\tVerbose:                      false,\n\t\tFail:                         false,\n\t\tStatusCode:                   \"unset\",\n\t\tStatusDescription:            \"\",\n\t\tVersion:                      \"unset\",\n\t}\n}\n\n// Config stores the runtime configuration for otel-cli.\n// Data structure is public so that it can serialize to json easily.\ntype Config struct {\n\tEndpoint       string            `json:\"endpoint\" env:\"OTEL_EXPORTER_OTLP_ENDPOINT\"`\n\tTracesEndpoint string            `json:\"traces_endpoint\" env:\"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT\"`\n\tProtocol       string            `json:\"protocol\" env:\"OTEL_EXPORTER_OTLP_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL\"`\n\tTimeout        string            `json:\"timeout\" env:\"OTEL_EXPORTER_OTLP_TIMEOUT,OTEL_EXPORTER_OTLP_TRACES_TIMEOUT\"`\n\tHeaders        map[string]string `json:\"otlp_headers\" env:\"OTEL_EXPORTER_OTLP_HEADERS\"` // TODO: needs json marshaler hook to mask tokens\n\tInsecure       bool              `json:\"insecure\" env:\"OTEL_EXPORTER_OTLP_INSECURE\"`\n\tBlocking       bool              `json:\"otlp_blocking\" env:\"OTEL_EXPORTER_OTLP_BLOCKING\"`\n\n\tTlsCACert     string `json:\"tls_ca_cert\" env:\"OTEL_EXPORTER_OTLP_CERTIFICATE,OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE\"`\n\tTlsClientKey  string `json:\"tls_client_key\" env:\"OTEL_EXPORTER_OTLP_CLIENT_KEY,OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY\"`\n\tTlsClientCert string `json:\"tls_client_cert\" env:\"OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE\"`\n\t// OTEL_CLI_NO_TLS_VERIFY is deprecated and will be removed for 1.0\n\tTlsNoVerify bool `json:\"tls_no_verify\" env:\"OTEL_CLI_TLS_NO_VERIFY,OTEL_CLI_NO_TLS_VERIFY\"`\n\n\tServiceName       string            `json:\"service_name\" env:\"OTEL_CLI_SERVICE_NAME,OTEL_SERVICE_NAME\"`\n\tSpanName          string            `json:\"span_name\" env:\"OTEL_CLI_SPAN_NAME\"`\n\tKind              string            `json:\"span_kind\" env:\"OTEL_CLI_TRACE_KIND\"`\n\tAttributes        map[string]string `json:\"span_attributes\" env:\"OTEL_CLI_ATTRIBUTES\"`\n\tStatusCode        string            `json:\"span_status_code\" env:\"OTEL_CLI_STATUS_CODE\"`\n\tStatusDescription string            `json:\"span_status_description\" env:\"OTEL_CLI_STATUS_DESCRIPTION\"`\n\tForceSpanId       string            `json:\"force_span_id\" env:\"OTEL_CLI_FORCE_SPAN_ID\"`\n\tForceParentSpanId string            `json:\"force_parent_span_id\" env:\"OTEL_CLI_FORCE_PARENT_SPAN_ID\"`\n\tForceTraceId      string            `json:\"force_trace_id\" env:\"OTEL_CLI_FORCE_TRACE_ID\"`\n\n\tTraceparentCarrierFile string `json:\"traceparent_carrier_file\" env:\"OTEL_CLI_CARRIER_FILE\"`\n\tTraceparentIgnoreEnv   bool   `json:\"traceparent_ignore_env\" env:\"OTEL_CLI_IGNORE_ENV\"`\n\tTraceparentPrint       bool   `json:\"traceparent_print\" env:\"OTEL_CLI_PRINT_TRACEPARENT\"`\n\tTraceparentPrintExport bool   `json:\"traceparent_print_export\" env:\"OTEL_CLI_EXPORT_TRACEPARENT\"`\n\tTraceparentRequired    bool   `json:\"traceparent_required\" env:\"OTEL_CLI_TRACEPARENT_REQUIRED\"`\n\n\tBackgroundParentPollMs       int    `json:\"background_parent_poll_ms\" env:\"\"`\n\tBackgroundSockdir            string `json:\"background_socket_directory\" env:\"\"`\n\tBackgroundWait               bool   `json:\"background_wait\" env:\"\"`\n\tBackgroundSkipParentPidCheck bool   `json:\"background_skip_parent_pid_check\"`\n\n\tExecCommandTimeout  string `json:\"exec_command_timeout\" env:\"OTEL_CLI_EXEC_CMD_TIMEOUT\"`\n\tExecTpDisableInject bool   `json:\"exec_tp_disable_inject\" env:\"OTEL_CLI_EXEC_TP_DISABLE_INJECT\"`\n\n\tStatusCanaryCount    int    `json:\"status_canary_count\"`\n\tStatusCanaryInterval string `json:\"status_canary_interval\"`\n\n\tSpanStartTime string `json:\"span_start_time\" env:\"\"`\n\tSpanEndTime   string `json:\"span_end_time\" env:\"\"`\n\tEventName     string `json:\"event_name\" env:\"\"`\n\tEventTime     string `json:\"event_time\" env:\"\"`\n\n\tCfgFile string `json:\"config_file\" env:\"OTEL_CLI_CONFIG_FILE\"`\n\tVerbose bool   `json:\"verbose\" env:\"OTEL_CLI_VERBOSE\"`\n\tFail    bool   `json:\"fail\" env:\"OTEL_CLI_FAIL\"`\n\n\t// not exported, used to get data from cobra to otlpclient internals\n\tVersion string `json:\"-\"`\n}\n\n// LoadFile reads the file specified by -c/--config and overwrites the\n// current config values with any found in the file.\nfunc (c *Config) LoadFile() error {\n\tif c.CfgFile == \"\" {\n\t\treturn nil\n\t}\n\n\tjs, err := os.ReadFile(c.CfgFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read file '%s': %w\", c.CfgFile, err)\n\t}\n\n\tif err := json.Unmarshal(js, c); err != nil {\n\t\treturn fmt.Errorf(\"failed to parse json data in file '%s': %w\", c.CfgFile, err)\n\t}\n\n\treturn nil\n}\n\n// LoadEnv loads environment variables into the config, overwriting current\n// values. Environment variable to config key mapping is tagged on the\n// Config struct. Multiple names for envvars is supported, comma-separated.\n// Takes a func(string)string that's usually os.Getenv, and is swappable to\n// make testing easier.\nfunc (c *Config) LoadEnv(getenv func(string) string) error {\n\t// loop over each field of the Config\n\tstructType := reflect.TypeOf(c).Elem()\n\tcValue := reflect.ValueOf(c).Elem()\n\tfor i := 0; i < structType.NumField(); i++ {\n\t\tfield := structType.Field(i)\n\t\tenvVars := field.Tag.Get(\"env\")\n\t\tif envVars == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// a field can have multiple comma-delimiated env vars to look in\n\t\tfor _, envVar := range strings.Split(envVars, \",\") {\n\t\t\t// call the provided func(string)string provided to get the\n\t\t\t// envvar, usually os.Getenv but can be a fake for testing\n\t\t\tenvVal := getenv(envVar)\n\n\t\t\tif envVal == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// type switch and write the value into the struct\n\t\t\ttarget := cValue.Field(i)\n\t\t\tswitch target.Interface().(type) {\n\t\t\tcase string:\n\t\t\t\ttarget.SetString(envVal)\n\t\t\tcase int:\n\t\t\t\tintVal, err := strconv.ParseInt(envVal, 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"could not parse %s value %q as an int: %w\", envVar, envVal, err)\n\t\t\t\t}\n\t\t\t\ttarget.SetInt(intVal)\n\t\t\tcase bool:\n\t\t\t\tboolVal, err := strconv.ParseBool(envVal)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"could not parse %s value %q as an bool: %w\", envVar, envVal, err)\n\t\t\t\t}\n\t\t\t\ttarget.SetBool(boolVal)\n\t\t\tcase map[string]string:\n\t\t\t\tmapVal, err := parseCkvStringMap(envVal)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"could not parse %s value %q as a map: %w\", envVar, envVal, err)\n\t\t\t\t}\n\t\t\t\tmapValVal := reflect.ValueOf(mapVal)\n\t\t\t\ttarget.Set(mapValVal)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ToStringMap flattens the configuration into a stringmap that is easy to work\n// with in tests especially with cmp.Diff. See test_main.go.\nfunc (c Config) ToStringMap() map[string]string {\n\treturn map[string]string{\n\t\t\"endpoint\":                    c.Endpoint,\n\t\t\"protocol\":                    c.Protocol,\n\t\t\"timeout\":                     c.Timeout,\n\t\t\"headers\":                     flattenStringMap(c.Headers, \"{}\"),\n\t\t\"insecure\":                    strconv.FormatBool(c.Insecure),\n\t\t\"blocking\":                    strconv.FormatBool(c.Blocking),\n\t\t\"tls_no_verify\":               strconv.FormatBool(c.TlsNoVerify),\n\t\t\"tls_ca_cert\":                 c.TlsCACert,\n\t\t\"tls_client_key\":              c.TlsClientKey,\n\t\t\"tls_client_cert\":             c.TlsClientCert,\n\t\t\"service_name\":                c.ServiceName,\n\t\t\"span_name\":                   c.SpanName,\n\t\t\"span_kind\":                   c.Kind,\n\t\t\"span_attributes\":             flattenStringMap(c.Attributes, \"{}\"),\n\t\t\"span_status_code\":            c.StatusCode,\n\t\t\"span_status_description\":     c.StatusDescription,\n\t\t\"traceparent_carrier_file\":    c.TraceparentCarrierFile,\n\t\t\"traceparent_ignore_env\":      strconv.FormatBool(c.TraceparentIgnoreEnv),\n\t\t\"traceparent_print\":           strconv.FormatBool(c.TraceparentPrint),\n\t\t\"traceparent_print_export\":    strconv.FormatBool(c.TraceparentPrintExport),\n\t\t\"traceparent_required\":        strconv.FormatBool(c.TraceparentRequired),\n\t\t\"background_parent_poll_ms\":   strconv.Itoa(c.BackgroundParentPollMs),\n\t\t\"background_socket_directory\": c.BackgroundSockdir,\n\t\t\"background_wait\":             strconv.FormatBool(c.BackgroundWait),\n\t\t\"background_skip_pid_check\":   strconv.FormatBool(c.BackgroundSkipParentPidCheck),\n\t\t\"exec_command_timeout\":        c.ExecCommandTimeout,\n\t\t\"exec_tp_disable_inject\":      strconv.FormatBool(c.ExecTpDisableInject),\n\t\t\"span_start_time\":             c.SpanStartTime,\n\t\t\"span_end_time\":               c.SpanEndTime,\n\t\t\"event_name\":                  c.EventName,\n\t\t\"event_time\":                  c.EventTime,\n\t\t\"config_file\":                 c.CfgFile,\n\t\t\"verbose\":                     strconv.FormatBool(c.Verbose),\n\t}\n}\n\n// GetIsRecording returns true if an endpoint is set and otel-cli expects to send real\n// spans. Returns false if unconfigured and going to run inert.\nfunc (c Config) GetIsRecording() bool {\n\tif c.Endpoint == \"\" && c.TracesEndpoint == \"\" {\n\t\tDiag.IsRecording = false\n\t\treturn false\n\t}\n\n\tDiag.IsRecording = true\n\treturn true\n}\n\n// ParseCliTimeout parses the --timeout string value to a time.Duration.\nfunc (c Config) ParseCliTimeout() time.Duration {\n\tout, err := parseDuration(c.Timeout)\n\tDiag.ParsedTimeoutMs = out.Milliseconds()\n\tc.SoftFailIfErr(err)\n\treturn out\n}\n\n// ParseExecCommandTimeout parses the --command-timeout string value to a time.Duration.\n// When timeout is unspecified or 0, otel-cli will wait forever for the command to complete.\nfunc (c Config) ParseExecCommandTimeout() time.Duration {\n\tif c.ExecCommandTimeout == \"\" {\n\t\treturn 0\n\t}\n\tout, err := parseDuration(c.ExecCommandTimeout)\n\tc.SoftFailIfErr(err)\n\treturn out\n}\n\n// ParseStatusCanaryInterval parses the --canary-interval string value to a time.Duration.\nfunc (c Config) ParseStatusCanaryInterval() time.Duration {\n\tout, err := parseDuration(c.StatusCanaryInterval)\n\tc.SoftFailIfErr(err)\n\treturn out\n}\n\n// parseDuration parses a string duration into a time.Duration.\n// When no duration letter is provided (e.g. ms, s, m, h), seconds are assumed.\n// It logs an error and returns time.Duration(0) if the string is empty or unparseable.\nfunc parseDuration(d string) (time.Duration, error) {\n\tvar out time.Duration\n\tif d == \"\" {\n\t\tout = time.Duration(0)\n\t} else if parsed, err := time.ParseDuration(d); err == nil {\n\t\tout = parsed\n\t} else if secs, serr := strconv.ParseInt(d, 10, 0); serr == nil {\n\t\tout = time.Second * time.Duration(secs)\n\t} else {\n\t\treturn time.Duration(0), fmt.Errorf(\"unable to parse duration string %q: %w\", d, err)\n\t}\n\n\treturn out, nil\n}\n\n// ParseEndpoint takes the endpoint or signal endpoint, augments as needed\n// (e.g. bare host:port for gRPC) and then parses as a URL.\n// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp\nfunc (config Config) ParseEndpoint() (*url.URL, string) {\n\tvar endpoint, source string\n\tvar epUrl *url.URL\n\tvar err error\n\n\t// signal-specific configs get precedence over general endpoint per OTel spec\n\tif config.TracesEndpoint != \"\" {\n\t\tendpoint = config.TracesEndpoint\n\t\tsource = \"signal\"\n\t} else if config.Endpoint != \"\" {\n\t\tendpoint = config.Endpoint\n\t\tsource = \"general\"\n\t} else {\n\t\tconfig.SoftFail(\"no endpoint configuration available\")\n\t}\n\n\tparts := strings.Split(endpoint, \":\")\n\t// bare hostname? can only be grpc, prepend\n\tif len(parts) == 1 {\n\t\tepUrl, err = url.Parse(\"grpc://\" + endpoint + \":4317\")\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"error parsing (assumed) gRPC bare host address '%s': %s\", endpoint, err)\n\t\t}\n\t} else if len(parts) > 1 { // could be URI or host:port\n\t\t// actual URIs\n\t\t// grpc:// is only an otel-cli thing, maybe should drop it?\n\t\tif parts[0] == \"grpc\" || parts[0] == \"http\" || parts[0] == \"https\" {\n\t\t\tepUrl, err = url.Parse(endpoint)\n\t\t\tif err != nil {\n\t\t\t\tconfig.SoftFail(\"error parsing provided %s URI '%s': %s\", source, endpoint, err)\n\t\t\t}\n\t\t} else {\n\t\t\t// gRPC host:port\n\t\t\tepUrl, err = url.Parse(\"grpc://\" + endpoint)\n\t\t\tif err != nil {\n\t\t\t\tconfig.SoftFail(\"error parsing (assumed) gRPC host:port address '%s': %s\", endpoint, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Per spec, /v1/traces is the default, appended to any url passed\n\t// to the general endpoint\n\tif strings.HasPrefix(epUrl.Scheme, \"http\") && source != \"signal\" && !strings.HasSuffix(epUrl.Path, \"/v1/traces\") {\n\t\tepUrl.Path = path.Join(epUrl.Path, \"/v1/traces\")\n\t}\n\n\tDiag.EndpointSource = source\n\tDiag.Endpoint = epUrl.String()\n\treturn epUrl, source\n}\n\n// SoftLog only calls through to log if otel-cli was run with the --verbose flag.\n// TODO: does it make any sense to support %w? probably yes, can clean up some\n// diagnostics.Error touch points.\nfunc (c Config) SoftLog(format string, a ...interface{}) {\n\tif !c.Verbose {\n\t\treturn\n\t}\n\tlog.Printf(format, a...)\n}\n\n// SoftLogIfErr calls SoftLog only if err != nil.\n// Written as an interim step to pushing errors up the stack instead of calling\n// SoftLog/SoftFail directly in methods that don't need a config handle.\nfunc (c Config) SoftLogIfErr(err error) {\n\tif err != nil {\n\t\tc.SoftLog(err.Error())\n\t}\n}\n\n// SoftFail calls through to softLog (which logs only if otel-cli was run with the --verbose\n// flag), then immediately exits - with status -1 by default, or 1 if --fail was\n// set (a la `curl --fail`)\nfunc (c Config) SoftFail(format string, a ...interface{}) {\n\tc.SoftLog(format, a...)\n\n\tif c.Fail {\n\t\tos.Exit(1)\n\t} else {\n\t\tos.Exit(0)\n\t}\n}\n\n// SoftFailIfErr calls SoftFail only if err != nil.\n// Written as an interim step to pushing errors up the stack instead of calling\n// SoftLog/SoftFail directly in methods that don't need a config handle.\nfunc (c Config) SoftFailIfErr(err error) {\n\tif err != nil {\n\t\tc.SoftFail(err.Error())\n\t}\n}\n\n// flattenStringMap takes a string map and returns it flattened into a string with\n// keys sorted lexically so it should be mostly consistent enough for comparisons\n// and printing. Output is k=v,k=v style like attributes input.\nfunc flattenStringMap(mp map[string]string, emptyValue string) string {\n\tif len(mp) == 0 {\n\t\treturn emptyValue\n\t}\n\n\tvar out string\n\tkeys := make([]string, len(mp)) // for sorting\n\tvar i int\n\tfor k := range mp {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\tsort.Strings(keys)\n\n\tfor i, k := range keys {\n\t\tout = out + k + \"=\" + mp[k]\n\t\tif i == len(keys)-1 {\n\t\t\tbreak\n\t\t}\n\t\tout = out + \",\"\n\t}\n\n\treturn out\n}\n\n// parseCkvStringMap parses key=value,foo=bar formatted strings as a line of CSV\n// and returns it as a string map.\nfunc parseCkvStringMap(in string) (map[string]string, error) {\n\tr := csv.NewReader(strings.NewReader(in))\n\tpairs, err := r.Read()\n\tif err != nil {\n\t\treturn map[string]string{}, err\n\t}\n\n\tout := make(map[string]string)\n\tfor _, pair := range pairs {\n\t\tparts := strings.SplitN(pair, \"=\", 2)\n\t\tif parts[0] != \"\" && parts[1] != \"\" {\n\t\t\tout[parts[0]] = parts[1]\n\t\t} else {\n\t\t\treturn map[string]string{}, fmt.Errorf(\"kv pair %s must be in key=value format\", pair)\n\t\t}\n\t}\n\n\treturn out, nil\n}\n\n// ParseSpanStartTime returns config.SpanStartTime as time.Time.\nfunc (c Config) ParseSpanStartTime() time.Time {\n\tt, err := c.parseTime(c.SpanStartTime, \"start\")\n\tc.SoftFailIfErr(err)\n\treturn t\n}\n\n// ParseSpanEndTime returns config.SpanEndTime as time.Time.\nfunc (c Config) ParseSpanEndTime() time.Time {\n\tt, err := c.parseTime(c.SpanEndTime, \"end\")\n\tc.SoftFailIfErr(err)\n\treturn t\n}\n\n// ParsedEventTime returns config.EventTime as time.Time.\nfunc (c Config) ParsedEventTime() time.Time {\n\tt, err := c.parseTime(c.EventTime, \"event\")\n\tc.SoftFailIfErr(err)\n\treturn t\n}\n\n// parseTime tries to parse Unix epoch, then RFC3339, both with/without nanoseconds\nfunc (c Config) parseTime(ts, which string) (time.Time, error) {\n\t// errors accumulate as parsing methods are attempted\n\t// thrown away when one succeeds, joined & returned if none succeed\n\terrs := []error{}\n\n\tif ts == \"now\" {\n\t\treturn time.Now(), nil\n\t}\n\n\t// Unix epoch time\n\tif i, err := strconv.ParseInt(ts, 10, 64); err == nil {\n\t\treturn time.Unix(i, 0), nil\n\t} else {\n\t\terrs = append(errs, fmt.Errorf(\"could not parse span %s time %q as Unix Epoch: %w\", which, ts, err))\n\t}\n\n\t// date --rfc-3339 returns an invalid format for Go because it has a\n\t// space instead of 'T' between date and time\n\tif detectBrokenRFC3339PrefixRe.MatchString(ts) {\n\t\tts = strings.Replace(ts, \" \", \"T\", 1)\n\t}\n\n\t// Unix epoch time with nanoseconds\n\tif epochNanoTimeRE.MatchString(ts) {\n\t\tparts := strings.Split(ts, \".\")\n\t\tif len(parts) == 2 {\n\t\t\tsecs, secsErr := strconv.ParseInt(parts[0], 10, 64)\n\t\t\tnsecs, usecsErr := strconv.ParseInt(parts[1], 10, 64)\n\t\t\tif secsErr == nil && usecsErr == nil && secs > 0 {\n\t\t\t\treturn time.Unix(secs, nsecs), nil\n\t\t\t} else {\n\t\t\t\terrs = append(errs, fmt.Errorf(\"could not parse span %s time %q as Unix Epoch: %w\", which, ts, secsErr))\n\t\t\t\terrs = append(errs, fmt.Errorf(\"could not parse span %s time %q as Unix Epoch.Nano: %w\", which, ts, usecsErr))\n\t\t\t}\n\t\t}\n\t}\n\n\t// try RFC3339 then again with nanos\n\tt, err := time.Parse(time.RFC3339, ts)\n\tif err != nil {\n\t\t// try again with nanos\n\t\tif t, innerErr := time.Parse(time.RFC3339Nano, ts); innerErr == nil {\n\t\t\treturn t, nil\n\t\t} else {\n\t\t\terrs = append(errs, fmt.Errorf(\"could not parse span %s time %q as RFC3339 %w\", which, ts, err))\n\t\t\terrs = append(errs, fmt.Errorf(\"could not parse span %s time %q as RFC3339Nano %w\", which, ts, innerErr))\n\t\t}\n\t} else {\n\t\treturn t, nil\n\t}\n\n\terrs = append(errs, fmt.Errorf(\"could not parse span %s time %q as any supported format\", which, ts))\n\treturn time.Time{}, errors.Join(errs...)\n}\n\nfunc (c Config) GetEndpoint() *url.URL {\n\tep, _ := c.ParseEndpoint()\n\treturn ep\n}\n\n// WithEndpoint returns the config with Endpoint set to the provided value.\nfunc (c Config) WithEndpoint(with string) Config {\n\tc.Endpoint = with\n\treturn c\n}\n\n// WithTracesEndpoint returns the config with TracesEndpoint set to the provided value.\nfunc (c Config) WithTracesEndpoint(with string) Config {\n\tc.TracesEndpoint = with\n\treturn c\n}\n\n// WithProtocol returns the config with protocol set to the provided value.\nfunc (c Config) WithProtocol(with string) Config {\n\tc.Protocol = with\n\treturn c\n}\n\n// GetTimeout returns the parsed --timeout value as a time.Duration.\nfunc (c Config) GetTimeout() time.Duration {\n\treturn c.ParseCliTimeout()\n}\n\n// WithTimeout returns the config with Timeout set to the provided value.\nfunc (c Config) WithTimeout(with string) Config {\n\tc.Timeout = with\n\treturn c\n}\n\n// GetHeaders returns the stringmap of configured headers.\nfunc (c Config) GetHeaders() map[string]string {\n\treturn c.Headers\n}\n\n// WithHeades returns the config with Heades set to the provided value.\nfunc (c Config) WithHeaders(with map[string]string) Config {\n\tc.Headers = with\n\treturn c\n}\n\n// WithInsecure returns the config with Insecure set to the provided value.\nfunc (c Config) WithInsecure(with bool) Config {\n\tc.Insecure = with\n\treturn c\n}\n\n// WithBlocking returns the config with Blocking set to the provided value.\nfunc (c Config) WithBlocking(with bool) Config {\n\tc.Blocking = with\n\treturn c\n}\n\n// WithTlsNoVerify returns the config with NoTlsVerify set to the provided value.\nfunc (c Config) WithTlsNoVerify(with bool) Config {\n\tc.TlsNoVerify = with\n\treturn c\n}\n\n// WithTlsCACert returns the config with TlsCACert set to the provided value.\nfunc (c Config) WithTlsCACert(with string) Config {\n\tc.TlsCACert = with\n\treturn c\n}\n\n// WithTlsClientKey returns the config with NoTlsClientKey set to the provided value.\nfunc (c Config) WithTlsClientKey(with string) Config {\n\tc.TlsClientKey = with\n\treturn c\n}\n\n// WithTlsClientCert returns the config with NoTlsClientCert set to the provided value.\nfunc (c Config) WithTlsClientCert(with string) Config {\n\tc.TlsClientCert = with\n\treturn c\n}\n\n// GetServiceName returns the configured OTel service name.\nfunc (c Config) GetServiceName() string {\n\treturn c.ServiceName\n}\n\n// WithServiceName returns the config with ServiceName set to the provided value.\nfunc (c Config) WithServiceName(with string) Config {\n\tc.ServiceName = with\n\treturn c\n}\n\n// WithSpanName returns the config with SpanName set to the provided value.\nfunc (c Config) WithSpanName(with string) Config {\n\tc.SpanName = with\n\treturn c\n}\n\n// WithKind returns the config with Kind set to the provided value.\nfunc (c Config) WithKind(with string) Config {\n\tc.Kind = with\n\treturn c\n}\n\n// WithAttributes returns the config with Attributes set to the provided value.\nfunc (c Config) WithAttributes(with map[string]string) Config {\n\tc.Attributes = with\n\treturn c\n}\n\n// WithStatusCode returns the config with StatusCode set to the provided value.\nfunc (c Config) WithStatusCode(with string) Config {\n\tc.StatusCode = with\n\treturn c\n}\n\n// WithStatusDescription returns the config with StatusDescription set to the provided value.\nfunc (c Config) WithStatusDescription(with string) Config {\n\tc.StatusDescription = with\n\treturn c\n}\n\n// WithTraceparentCarrierFile returns the config with TraceparentCarrierFile set to the provided value.\nfunc (c Config) WithTraceparentCarrierFile(with string) Config {\n\tc.TraceparentCarrierFile = with\n\treturn c\n}\n\n// WithTraceparentIgnoreEnv returns the config with TraceparentIgnoreEnv set to the provided value.\nfunc (c Config) WithTraceparentIgnoreEnv(with bool) Config {\n\tc.TraceparentIgnoreEnv = with\n\treturn c\n}\n\n// WithTraceparentPrint returns the config with TraceparentPrint set to the provided value.\nfunc (c Config) WithTraceparentPrint(with bool) Config {\n\tc.TraceparentPrint = with\n\treturn c\n}\n\n// WithTraceparentPrintExport returns the config with TraceparentPrintExport set to the provided value.\nfunc (c Config) WithTraceparentPrintExport(with bool) Config {\n\tc.TraceparentPrintExport = with\n\treturn c\n}\n\n// WithTraceparentRequired returns the config with TraceparentRequired set to the provided value.\nfunc (c Config) WithTraceparentRequired(with bool) Config {\n\tc.TraceparentRequired = with\n\treturn c\n}\n\n// WithBackgroundParentPollMs returns the config with BackgroundParentPollMs set to the provided value.\nfunc (c Config) WithBackgroundParentPollMs(with int) Config {\n\tc.BackgroundParentPollMs = with\n\treturn c\n}\n\n// WithBackgroundSockdir returns the config with BackgroundSockdir set to the provided value.\nfunc (c Config) WithBackgroundSockdir(with string) Config {\n\tc.BackgroundSockdir = with\n\treturn c\n}\n\n// WithBackgroundWait returns the config with BackgroundWait set to the provided value.\nfunc (c Config) WithBackgroundWait(with bool) Config {\n\tc.BackgroundWait = with\n\treturn c\n}\n\n// WithBackgroundSkipParentPidCheck returns the config with BackgroundSkipParentPidCheck set to the provided value.\nfunc (c Config) WithBackgroundSkipParentPidCheck(with bool) Config {\n\tc.BackgroundSkipParentPidCheck = with\n\treturn c\n}\n\n// WithStatusCanaryCount returns the config with StatusCanaryCount set to the provided value.\nfunc (c Config) WithStatusCanaryCount(with int) Config {\n\tc.StatusCanaryCount = with\n\treturn c\n}\n\n// WithStatusCanaryInterval returns the config with StatusCanaryInterval set to the provided value.\nfunc (c Config) WithStatusCanaryInterval(with string) Config {\n\tc.StatusCanaryInterval = with\n\treturn c\n}\n\n// WithSpanStartTime returns the config with SpanStartTime set to the provided value.\nfunc (c Config) WithSpanStartTime(with string) Config {\n\tc.SpanStartTime = with\n\treturn c\n}\n\n// WithSpanEndTime returns the config with SpanEndTime set to the provided value.\nfunc (c Config) WithSpanEndTime(with string) Config {\n\tc.SpanEndTime = with\n\treturn c\n}\n\n// WithEventName returns the config with EventName set to the provided value.\nfunc (c Config) WithEventName(with string) Config {\n\tc.EventName = with\n\treturn c\n}\n\n// WithEventTIme returns the config with EventTIme set to the provided value.\nfunc (c Config) WithEventTime(with string) Config {\n\tc.EventTime = with\n\treturn c\n}\n\n// WithCfgFile returns the config with CfgFile set to the provided value.\nfunc (c Config) WithCfgFile(with string) Config {\n\tc.CfgFile = with\n\treturn c\n}\n\n// WithVerbose returns the config with Verbose set to the provided value.\nfunc (c Config) WithVerbose(with bool) Config {\n\tc.Verbose = with\n\treturn c\n}\n\n// WithFail returns the config with Fail set to the provided value.\nfunc (c Config) WithFail(with bool) Config {\n\tc.Fail = with\n\treturn c\n}\n\n// Version returns the program version stored in the config.\nfunc (c Config) GetVersion() string {\n\treturn c.Version\n}\n\n// WithVersion returns the config with Version set to the provided value.\nfunc (c Config) WithVersion(with string) Config {\n\tc.Version = with\n\treturn c\n}\n"
  },
  {
    "path": "otelcli/config_span.go",
    "content": "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.com/equinix-labs/otel-cli/w3c/traceparent\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// NewProtobufSpan creates a new span and populates it with information\n// from the config struct.\nfunc (c Config) NewProtobufSpan() *tracepb.Span {\n\tspan := otlpclient.NewProtobufSpan()\n\tif c.GetIsRecording() {\n\t\tspan.TraceId = otlpclient.GenerateTraceId()\n\t\tspan.SpanId = otlpclient.GenerateSpanId()\n\t}\n\tspan.Name = c.SpanName\n\tspan.Kind = otlpclient.SpanKindStringToInt(c.Kind)\n\tspan.Attributes = otlpclient.StringMapAttrsToProtobuf(c.Attributes)\n\n\tnow := time.Now()\n\tif c.SpanStartTime != \"\" {\n\t\tst := c.ParseSpanStartTime()\n\t\tspan.StartTimeUnixNano = uint64(st.UnixNano())\n\t} else {\n\t\tspan.StartTimeUnixNano = uint64(now.UnixNano())\n\t}\n\n\tif c.SpanEndTime != \"\" {\n\t\tet := c.ParseSpanEndTime()\n\t\tspan.EndTimeUnixNano = uint64(et.UnixNano())\n\t} else {\n\t\tspan.EndTimeUnixNano = uint64(now.UnixNano())\n\t}\n\n\tif c.GetIsRecording() {\n\t\ttp := c.LoadTraceparent()\n\t\tif tp.Initialized {\n\t\t\tspan.TraceId = tp.TraceId\n\t\t\tspan.ParentSpanId = tp.SpanId\n\t\t}\n\t} else {\n\t\tspan.TraceId = otlpclient.GetEmptyTraceId()\n\t\tspan.SpanId = otlpclient.GetEmptySpanId()\n\t}\n\n\t// --force-trace-id, --force-span-id and --force-parent-span-id let the user set their own trace, span & parent span ids\n\t// these work in non-recording mode and will stomp trace id from the traceparent\n\tvar err error\n\tif c.ForceTraceId != \"\" {\n\t\tspan.TraceId, err = parseHex(c.ForceTraceId, 16)\n\t\tc.SoftFailIfErr(err)\n\t}\n\tif c.ForceSpanId != \"\" {\n\t\tspan.SpanId, err = parseHex(c.ForceSpanId, 8)\n\t\tc.SoftFailIfErr(err)\n\t}\n\tif c.ForceParentSpanId != \"\" {\n\t\tspan.ParentSpanId, err = parseHex(c.ForceParentSpanId, 8)\n\t\tc.SoftFailIfErr(err)\n\t}\n\n\totlpclient.SetSpanStatus(span, c.StatusCode, c.StatusDescription)\n\n\treturn span\n}\n\n// LoadTraceparent follows otel-cli's loading rules, start with envvar then file.\n// If both are set, the file will override env.\n// When in non-recording mode, the previous traceparent will be returned if it's\n// available, otherwise, a zero-valued traceparent is returned.\nfunc (c Config) LoadTraceparent() traceparent.Traceparent {\n\ttp := traceparent.Traceparent{\n\t\tVersion:     0,\n\t\tTraceId:     otlpclient.GetEmptyTraceId(),\n\t\tSpanId:      otlpclient.GetEmptySpanId(),\n\t\tSampling:    false,\n\t\tInitialized: true,\n\t}\n\n\tif !c.TraceparentIgnoreEnv {\n\t\tvar err error\n\t\ttp, err = traceparent.LoadFromEnv()\n\t\tif err != nil {\n\t\t\tDiag.Error = err.Error()\n\t\t}\n\t}\n\n\tif c.TraceparentCarrierFile != \"\" {\n\t\tfileTp, err := traceparent.LoadFromFile(c.TraceparentCarrierFile)\n\t\tif err != nil {\n\t\t\tDiag.Error = err.Error()\n\t\t} else if fileTp.Initialized {\n\t\t\ttp = fileTp\n\t\t}\n\t}\n\n\tif c.TraceparentRequired {\n\t\tif tp.Initialized {\n\t\t\treturn tp\n\t\t} else {\n\t\t\tc.SoftFail(\"failed to find a valid traceparent carrier in either environment for file '%s' while it's required by --tp-required\", c.TraceparentCarrierFile)\n\t\t}\n\t}\n\n\treturn tp\n}\n\n// PropagateTraceparent saves the traceparent to file if necessary, then prints\n// span info to the console according to command-line args.\nfunc (c Config) PropagateTraceparent(span *tracepb.Span, target io.Writer) {\n\tvar tp traceparent.Traceparent\n\tif c.GetIsRecording() {\n\t\ttp = otlpclient.TraceparentFromProtobufSpan(span, c.GetIsRecording())\n\t} else {\n\t\t// when in non-recording mode, and there is a TP available, propagate that\n\t\ttp = c.LoadTraceparent()\n\t}\n\n\tif c.TraceparentCarrierFile != \"\" {\n\t\terr := tp.SaveToFile(c.TraceparentCarrierFile, c.TraceparentPrintExport)\n\t\tc.SoftFailIfErr(err)\n\t}\n\n\tif c.TraceparentPrint {\n\t\ttp.Fprint(target, c.TraceparentPrintExport)\n\t}\n}\n\n// parseHex parses hex into a []byte of length provided. Errors if the input is\n// not valid hex or the converted hex is not the right number of bytes.\nfunc parseHex(in string, expectedLen int) ([]byte, error) {\n\tout, err := hex.DecodeString(in)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing hex string %q: %w\", in, err)\n\t}\n\tif len(out) != expectedLen {\n\t\treturn nil, fmt.Errorf(\"hex string %q is the wrong length, expected %d bytes but got %d\", in, expectedLen, len(out))\n\t}\n\treturn out, nil\n}\n"
  },
  {
    "path": "otelcli/config_span_test.go",
    "content": "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/otlpclient\"\n)\n\nfunc TestPropagateTraceparent(t *testing.T) {\n\tconfig := DefaultConfig().\n\t\tWithTraceparentCarrierFile(\"\").\n\t\tWithTraceparentPrint(false).\n\t\tWithTraceparentPrintExport(false)\n\n\ttp := \"00-3433d5ae39bdfee397f44be5146867b3-8a5518f1e5c54d0a-01\"\n\ttid := \"3433d5ae39bdfee397f44be5146867b3\"\n\tsid := \"8a5518f1e5c54d0a\"\n\tos.Setenv(\"TRACEPARENT\", tp)\n\n\tspan := otlpclient.NewProtobufSpan()\n\tspan.TraceId, _ = hex.DecodeString(tid)\n\tspan.SpanId, _ = hex.DecodeString(sid)\n\n\tbuf := new(bytes.Buffer)\n\tconfig.PropagateTraceparent(span, buf)\n\tif buf.Len() != 0 {\n\t\tt.Errorf(\"nothing was supposed to be written but %d bytes were\", buf.Len())\n\t}\n\n\tconfig.TraceparentPrint = true\n\tconfig.TraceparentPrintExport = true\n\tbuf = new(bytes.Buffer)\n\tconfig.PropagateTraceparent(span, buf)\n\tif buf.Len() == 0 {\n\t\tt.Error(\"expected more than zero bytes but got none\")\n\t}\n\texpected := fmt.Sprintf(\"# trace id: %s\\n#  span id: %s\\nexport TRACEPARENT=%s\\n\", tid, sid, tp)\n\tif buf.String() != expected {\n\t\tt.Errorf(\"got unexpected output, expected '%s', got '%s'\", expected, buf.String())\n\t}\n}\n\nfunc TestNewProtobufSpanWithConfig(t *testing.T) {\n\tc := DefaultConfig().WithSpanName(\"test span 123\")\n\tspan := c.NewProtobufSpan()\n\n\tif span.Name != \"test span 123\" {\n\t\tt.Error(\"span event attributes must not be nil\")\n\t}\n}\n"
  },
  {
    "path": "otelcli/config_test.go",
    "content": "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.T) {\n\tc := Config{}\n\tc.Headers = map[string]string{\n\t\t\"123test\": \"deadbeefcafe\",\n\t}\n\n\tfsm := c.ToStringMap()\n\n\tif _, ok := fsm[\"headers\"]; !ok {\n\t\tt.Errorf(\"missing key 'headers' in returned string map: %q\", fsm)\n\t\tt.Fail()\n\t}\n\n\tif fsm[\"headers\"] != \"123test=deadbeefcafe\" {\n\t\tt.Errorf(\"expected header value not found in flattened string map: %q\", fsm)\n\t\tt.Fail()\n\t}\n}\n\nfunc TestIsRecording(t *testing.T) {\n\tc := DefaultConfig()\n\tif c.GetIsRecording() {\n\t\tt.Fail()\n\t}\n\tc.Endpoint = \"https://localhost:4318\"\n\n\tif !c.GetIsRecording() {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestFlattenStringMap(t *testing.T) {\n\tin := map[string]string{\n\t\t\"sample1\": \"value1\",\n\t\t\"more\":    \"stuff\",\n\t\t\"getting\": \"bored\",\n\t\t\"okay\":    \"that's enough\",\n\t}\n\n\tout := flattenStringMap(in, \"{}\")\n\n\tif out != \"getting=bored,more=stuff,okay=that's enough,sample1=value1\" {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestParseCkvStringMap(t *testing.T) {\n\texpect := map[string]string{\n\t\t\"sample1\": \"value1\",\n\t\t\"more\":    \"stuff\",\n\t\t\"getting\": \"bored\",\n\t\t\"okay\":    \"that's enough\",\n\t\t\"1\":       \"324\",\n\t}\n\n\tgot, err := parseCkvStringMap(\"1=324,getting=bored,more=stuff,okay=that's enough,sample1=value1\")\n\tif err != nil {\n\t\tt.Errorf(\"error on valid input: %s\", err)\n\t}\n\n\tif diff := cmp.Diff(expect, got); diff != \"\" {\n\t\tt.Errorf(\"maps didn't match (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestParseTime(t *testing.T) {\n\tmustParse := func(layout, value string) time.Time {\n\t\tout, err := time.Parse(layout, value)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to parse time '%s' as format '%s': %s\", value, layout, err)\n\t\t}\n\t\treturn out\n\t}\n\n\tfor _, testcase := range []struct {\n\t\tname  string\n\t\tinput string\n\t\twant  time.Time\n\t}{\n\t\t{\n\t\t\tname:  \"Unix epoch time without nanoseconds\",\n\t\t\tinput: \"1617739561\", // date +%s\n\t\t\twant:  time.Unix(1617739561, 0),\n\t\t},\n\t\t{\n\t\t\tname:  \"Unix epoch time with nanoseconds\",\n\t\t\tinput: \"1617739615.759793032\", // date +%s.%N\n\t\t\twant:  time.Unix(1617739615, 759793032),\n\t\t},\n\t\t{\n\t\t\tname:  \"RFC3339\",\n\t\t\tinput: \"2021-04-06T13:07:54Z\",\n\t\t\twant:  mustParse(time.RFC3339, \"2021-04-06T13:07:54Z\"),\n\t\t},\n\t\t{\n\t\t\tname:  \"RFC3339 with nanoseconds\",\n\t\t\tinput: \"2021-04-06T13:12:40.792426395Z\",\n\t\t\twant:  mustParse(time.RFC3339Nano, \"2021-04-06T13:12:40.792426395Z\"),\n\t\t},\n\t\t// date(1) RFC3339 format is incompatible with Go's formats\n\t\t// so parseTime takes care of that automatically\n\t\t{\n\t\t\tname:  \"date(1) RFC3339 output, with timezone\",\n\t\t\tinput: \"2021-04-06 13:07:54-07:00\", //date --rfc-3339=seconds\n\t\t\twant:  mustParse(time.RFC3339, \"2021-04-06T13:07:54-07:00\"),\n\t\t},\n\t\t{\n\t\t\tname:  \"date(1) RFC3339 with nanoseconds and timezone\",\n\t\t\tinput: \"2021-04-06 13:12:40.792426395-07:00\", // date --rfc-3339=ns\n\t\t\twant:  mustParse(time.RFC3339Nano, \"2021-04-06T13:12:40.792426395-07:00\"),\n\t\t},\n\t\t// TODO: maybe refactor parseTime to make failures easier to validate?\n\t\t// @tobert: gonna leave that for functional tests for now\n\t} {\n\t\tt.Run(testcase.name, func(t *testing.T) {\n\t\t\tout, _ := DefaultConfig().parseTime(testcase.input, \"test\")\n\t\t\tif !out.Equal(testcase.want) {\n\t\t\t\tt.Errorf(\"got wrong time from parseTime: %s\", out.Format(time.RFC3339Nano))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseCliTime(t *testing.T) {\n\tfor _, testcase := range []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected time.Duration\n\t}{\n\t\t// otel-cli will still timeout but it will be the default timeouts for\n\t\t// each component\n\t\t{\n\t\t\tname:     \"empty string returns 0 duration\",\n\t\t\tinput:    \"\",\n\t\t\texpected: time.Duration(0),\n\t\t},\n\t\t{\n\t\t\tname:     \"0 returns 0 duration\",\n\t\t\tinput:    \"0\",\n\t\t\texpected: time.Duration(0),\n\t\t},\n\t\t{\n\t\t\tname:     \"1s returns 1 second\",\n\t\t\tinput:    \"1s\",\n\t\t\texpected: time.Second,\n\t\t},\n\t\t{\n\t\t\tname:     \"100ms returns 100 milliseconds\",\n\t\t\tinput:    \"100ms\",\n\t\t\texpected: time.Millisecond * 100,\n\t\t},\n\t} {\n\t\tt.Run(testcase.name, func(t *testing.T) {\n\t\t\tconfig := DefaultConfig().WithTimeout(testcase.input)\n\t\t\tgot := config.ParseCliTimeout()\n\t\t\tif got != testcase.expected {\n\t\t\t\ted := testcase.expected.String()\n\t\t\t\tgd := got.String()\n\t\t\t\tt.Errorf(\"duration string %q was expected to return %s but returned %s\", config.Timeout, ed, gd)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseEndpoint(t *testing.T) {\n\t// func parseEndpoint(config Config) (*url.URL, string) {\n\n\tfor _, tc := range []struct {\n\t\tconfig       Config\n\t\twantEndpoint string\n\t\twantSource   string\n\t}{\n\t\t// gRPC, general, bare host\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithEndpoint(\"localhost\"),\n\t\t\twantEndpoint: \"grpc://localhost:4317\",\n\t\t\twantSource:   \"general\",\n\t\t},\n\t\t// gRPC, general, should be bare host:port\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithEndpoint(\"localhost:4317\"),\n\t\t\twantEndpoint: \"grpc://localhost:4317\",\n\t\t\twantSource:   \"general\",\n\t\t},\n\t\t// gRPC, general, https URL, should transform to host:port\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithEndpoint(\"https://localhost:4317\").WithProtocol(\"grpc\"),\n\t\t\twantEndpoint: \"https://localhost:4317/v1/traces\",\n\t\t\twantSource:   \"general\",\n\t\t},\n\t\t// HTTP, general, with a provided default signal path, should not be modified\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithEndpoint(\"http://localhost:9999/v1/traces\"),\n\t\t\twantEndpoint: \"http://localhost:9999/v1/traces\",\n\t\t\twantSource:   \"general\",\n\t\t},\n\t\t// HTTP, general, with a provided custom signal path, signal path should get appended\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithEndpoint(\"http://localhost:9999/my/collector/path\"),\n\t\t\twantEndpoint: \"http://localhost:9999/my/collector/path/v1/traces\",\n\t\t\twantSource:   \"general\",\n\t\t},\n\t\t// HTTPS, general, without path, should get /v1/traces appended\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithEndpoint(\"https://localhost:4317\"),\n\t\t\twantEndpoint: \"https://localhost:4317/v1/traces\",\n\t\t\twantSource:   \"general\",\n\t\t},\n\t\t// gRPC, signal, should come through with just the grpc:// added\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithTracesEndpoint(\"localhost\"),\n\t\t\twantEndpoint: \"grpc://localhost:4317\",\n\t\t\twantSource:   \"signal\",\n\t\t},\n\t\t// http, signal, should come through unmodified\n\t\t{\n\t\t\tconfig:       DefaultConfig().WithTracesEndpoint(\"http://localhost\"),\n\t\t\twantEndpoint: \"http://localhost\",\n\t\t\twantSource:   \"signal\",\n\t\t},\n\t} {\n\t\tu, src := tc.config.ParseEndpoint()\n\n\t\tif u.String() != tc.wantEndpoint {\n\t\t\tt.Errorf(\"Expected endpoint %q but got %q\", tc.wantEndpoint, u.String())\n\t\t}\n\n\t\tif src != tc.wantSource {\n\t\t\tt.Errorf(\"Expected source %q for test url %q but got %q\", tc.wantSource, u.String(), src)\n\t\t}\n\t}\n}\n\nfunc TestWithEndpoint(t *testing.T) {\n\tif DefaultConfig().WithEndpoint(\"foobar\").Endpoint != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTracesEndpoint(t *testing.T) {\n\tif DefaultConfig().WithTracesEndpoint(\"foobar\").TracesEndpoint != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTimeout(t *testing.T) {\n\tif DefaultConfig().WithTimeout(\"foobar\").Timeout != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithHeaders(t *testing.T) {\n\tattr := map[string]string{\"foo\": \"bar\"}\n\tc := DefaultConfig().WithHeaders(attr)\n\tif diff := cmp.Diff(attr, c.Headers); diff != \"\" {\n\t\tt.Errorf(\"Headers did not match (-want +got):\\n%s\", diff)\n\t}\n}\nfunc TestWithInsecure(t *testing.T) {\n\tif DefaultConfig().WithInsecure(true).Insecure != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithBlocking(t *testing.T) {\n\tif DefaultConfig().WithBlocking(true).Blocking != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTlsNoVerify(t *testing.T) {\n\tif DefaultConfig().WithTlsNoVerify(true).TlsNoVerify != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTlsCACert(t *testing.T) {\n\tif DefaultConfig().WithTlsCACert(\"/a/b/c\").TlsCACert != \"/a/b/c\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTlsClientKey(t *testing.T) {\n\tif DefaultConfig().WithTlsClientKey(\"/c/b/a\").TlsClientKey != \"/c/b/a\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTlsClientCert(t *testing.T) {\n\tif DefaultConfig().WithTlsClientCert(\"/b/c/a\").TlsClientCert != \"/b/c/a\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithServiceName(t *testing.T) {\n\tif DefaultConfig().WithServiceName(\"foobar\").ServiceName != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithSpanName(t *testing.T) {\n\tif DefaultConfig().WithSpanName(\"foobar\").SpanName != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithKind(t *testing.T) {\n\tif DefaultConfig().WithKind(\"producer\").Kind != \"producer\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithAttributes(t *testing.T) {\n\tattr := map[string]string{\"foo\": \"bar\"}\n\tc := DefaultConfig().WithAttributes(attr)\n\tif diff := cmp.Diff(attr, c.Attributes); diff != \"\" {\n\t\tt.Errorf(\"Attributes did not match (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestWithStatusCode(t *testing.T) {\n\tif diff := cmp.Diff(DefaultConfig().WithStatusCode(\"unset\").StatusCode, \"unset\"); diff != \"\" {\n\t\tt.Fatalf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n\n\tif diff := cmp.Diff(DefaultConfig().WithStatusCode(\"ok\").StatusCode, \"ok\"); diff != \"\" {\n\t\tt.Fatalf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n\n\tif diff := cmp.Diff(DefaultConfig().WithStatusCode(\"error\").StatusCode, \"error\"); diff != \"\" {\n\t\tt.Fatalf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestWithStatusDescription(t *testing.T) {\n\tif diff := cmp.Diff(DefaultConfig().WithStatusDescription(\"Set SCE To AUX\").StatusDescription, \"Set SCE To AUX\"); diff != \"\" {\n\t\tt.Fatalf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestWithTraceparentCarrierFile(t *testing.T) {\n\tif DefaultConfig().WithTraceparentCarrierFile(\"foobar\").TraceparentCarrierFile != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTraceparentIgnoreEnv(t *testing.T) {\n\tif DefaultConfig().WithTraceparentIgnoreEnv(true).TraceparentIgnoreEnv != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTraceparentPrint(t *testing.T) {\n\tif DefaultConfig().WithTraceparentPrint(true).TraceparentPrint != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTraceparentPrintExport(t *testing.T) {\n\tif DefaultConfig().WithTraceparentPrintExport(true).TraceparentPrintExport != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithTraceparentRequired(t *testing.T) {\n\tif DefaultConfig().WithTraceparentRequired(true).TraceparentRequired != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithBackgroundParentPollMs(t *testing.T) {\n\tif DefaultConfig().WithBackgroundParentPollMs(1111).BackgroundParentPollMs != 1111 {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithBackgroundSockdir(t *testing.T) {\n\tif DefaultConfig().WithBackgroundSockdir(\"foobar\").BackgroundSockdir != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithBackgroundWait(t *testing.T) {\n\tif DefaultConfig().WithBackgroundWait(true).BackgroundWait != true {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithStatusCanaryCount(t *testing.T) {\n\tif DefaultConfig().WithStatusCanaryCount(1337).StatusCanaryCount != 1337 {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithStatusCanaryInterval(t *testing.T) {\n\tif DefaultConfig().WithStatusCanaryInterval(\"1337ms\").StatusCanaryInterval != \"1337ms\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithSpanStartTime(t *testing.T) {\n\tif DefaultConfig().WithSpanStartTime(\"foobar\").SpanStartTime != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithSpanEndTime(t *testing.T) {\n\tif DefaultConfig().WithSpanEndTime(\"foobar\").SpanEndTime != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithEventName(t *testing.T) {\n\tif DefaultConfig().WithEventName(\"foobar\").EventName != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithEventTime(t *testing.T) {\n\tif DefaultConfig().WithEventTime(\"foobar\").EventTime != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithCfgFile(t *testing.T) {\n\tif DefaultConfig().WithCfgFile(\"foobar\").CfgFile != \"foobar\" {\n\t\tt.Fail()\n\t}\n}\nfunc TestWithVerbose(t *testing.T) {\n\tif DefaultConfig().WithVerbose(true).Verbose != true {\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "otelcli/config_tls.go",
    "content": "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 configuration and returns a tls.Config\n// that can be used by grpc or https.\nfunc (config Config) GetTlsConfig() *tls.Config {\n\ttlsConfig := &tls.Config{}\n\n\tif config.TlsNoVerify {\n\t\tDiag.InsecureSkipVerify = true\n\t\ttlsConfig.InsecureSkipVerify = true\n\t}\n\n\t// puts the provided CA certificate into the root pool\n\t// when not provided, Go TLS will automatically load the system CA pool\n\tif config.TlsCACert != \"\" {\n\t\tdata, err := os.ReadFile(config.TlsCACert)\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"failed to load CA certificate: %s\", err)\n\t\t}\n\n\t\tcertpool := x509.NewCertPool()\n\t\tcertpool.AppendCertsFromPEM(data)\n\t\ttlsConfig.RootCAs = certpool\n\t}\n\n\t// client certificate authentication\n\tif config.TlsClientCert != \"\" && config.TlsClientKey != \"\" {\n\t\tclientPEM, err := os.ReadFile(config.TlsClientCert)\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"failed to read client certificate file %s: %s\", config.TlsClientCert, err)\n\t\t}\n\t\tclientKeyPEM, err := os.ReadFile(config.TlsClientKey)\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"failed to read client key file %s: %s\", config.TlsClientKey, err)\n\t\t}\n\t\tcertPair, err := tls.X509KeyPair(clientPEM, clientKeyPEM)\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"failed to parse client cert pair: %s\", err)\n\t\t}\n\t\ttlsConfig.Certificates = []tls.Certificate{certPair}\n\t} else if config.TlsClientCert != \"\" {\n\t\tconfig.SoftFail(\"client cert and key must be specified together\")\n\t} else if config.TlsClientKey != \"\" {\n\t\tconfig.SoftFail(\"client cert and key must be specified together\")\n\t}\n\n\treturn tlsConfig\n}\n\n// GetInsecure returns true if the configuration expects a non-TLS connection.\nfunc (c Config) GetInsecure() bool {\n\tendpointURL := c.GetEndpoint()\n\n\tisLoopback, err := isLoopbackAddr(endpointURL)\n\tc.SoftFailIfErr(err)\n\n\t// Go's TLS does the right thing and forces us to say we want to disable encryption,\n\t// but I expect most users of this program to point at a localhost endpoint that might not\n\t// have any encryption available, or setting it up raises the bar of entry too high.\n\t// The compromise is to automatically flip this flag to true when endpoint contains an\n\t// an obvious \"localhost\", \"127.0.0.x\", or \"::1\" address.\n\tif c.Insecure || (isLoopback && endpointURL.Scheme != \"https\") {\n\t\treturn true\n\t} else if endpointURL.Scheme == \"http\" || endpointURL.Scheme == \"unix\" {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// isLoopbackAddr takes a url.URL, looks up the address, then returns true\n// if it points at either a v4 or v6 loopback address.\n// As I understood the OTLP spec, only host:port or an HTTP URL are acceptable.\n// This function is _not_ meant to validate the endpoint, that will happen when\n// otel-go attempts to connect to the endpoint.\nfunc isLoopbackAddr(u *url.URL) (bool, error) {\n\thostname := u.Hostname()\n\n\tif hostname == \"localhost\" || hostname == \"127.0.0.1\" || hostname == \"::1\" {\n\t\tDiag.DetectedLocalhost = true\n\t\treturn true, nil\n\t}\n\n\tips, err := net.LookupIP(hostname)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to look up hostname '%s': %s\", hostname, err)\n\t}\n\n\t// all ips returned must be loopback to return true\n\t// cases where that isn't true should be super rare, and probably all shenanigans\n\tallAreLoopback := true\n\tfor _, ip := range ips {\n\t\tif !ip.IsLoopback() {\n\t\t\tallAreLoopback = false\n\t\t}\n\t}\n\n\tDiag.DetectedLocalhost = allAreLoopback\n\treturn allAreLoopback, nil\n}\n"
  },
  {
    "path": "otelcli/diagnostics.go",
    "content": "package otelcli\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// package global Diagnostics handle, written to from all over otel-cli\n// and used in e.g. otel-cli status to show internal state\nvar Diag Diagnostics\n\n// Diagnostics is a place to put things that are useful for testing and\n// diagnosing issues with otel-cli. The only user-facing feature that should be\n// using these is otel-cli status.\ntype Diagnostics struct {\n\tCliArgs            []string `json:\"cli_args\"`\n\tIsRecording        bool     `json:\"is_recording\"`\n\tConfigFileLoaded   bool     `json:\"config_file_loaded\"`\n\tNumArgs            int      `json:\"number_of_args\"`\n\tDetectedLocalhost  bool     `json:\"detected_localhost\"`\n\tInsecureSkipVerify bool     `json:\"insecure_skip_verify\"`\n\tParsedTimeoutMs    int64    `json:\"parsed_timeout_ms\"`\n\tEndpoint           string   `json:\"endpoint\"` // the computed endpoint, not the raw config val\n\tEndpointSource     string   `json:\"endpoint_source\"`\n\tError              string   `json:\"error\"`\n\tExecExitCode       int      `json:\"exec_exit_code\"`\n\tRetries            int      `json:\"retries\"`\n}\n\n// ToMap returns the Diag struct as a string map for testing.\nfunc (d *Diagnostics) ToStringMap() map[string]string {\n\treturn map[string]string{\n\t\t\"cli_args\":           strings.Join(d.CliArgs, \" \"),\n\t\t\"is_recording\":       strconv.FormatBool(d.IsRecording),\n\t\t\"config_file_loaded\": strconv.FormatBool(d.ConfigFileLoaded),\n\t\t\"number_of_args\":     strconv.Itoa(d.NumArgs),\n\t\t\"detected_localhost\": strconv.FormatBool(d.DetectedLocalhost),\n\t\t\"parsed_timeout_ms\":  strconv.FormatInt(d.ParsedTimeoutMs, 10),\n\t\t\"endpoint\":           d.Endpoint,\n\t\t\"endpoint_source\":    d.EndpointSource,\n\t\t\"error\":              d.Error,\n\t}\n}\n\n// SetError sets the diagnostics Error to the error's string if it's\n// not nil and returns the same error so it can be inlined in return.\nfunc (d *Diagnostics) SetError(err error) error {\n\tif err != nil {\n\t\tDiag.Error = err.Error()\n\t}\n\treturn err\n}\n\n// GetExitCode() is a helper for Cobra to retrieve the exit code, mainly\n// used by exec to make otel-cli return the child program's exit code.\nfunc GetExitCode() int {\n\treturn Diag.ExecExitCode\n}\n"
  },
  {
    "path": "otelcli/exec.go",
    "content": "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/equinix-labs/otel-cli/otlpclient\"\n\t\"github.com/equinix-labs/otel-cli/w3c/traceparent\"\n\t\"github.com/spf13/cobra\"\n\tcommonpb \"go.opentelemetry.io/proto/otlp/common/v1\"\n\ttracev1 \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// execCmd sets up the `otel-cli exec` command\nfunc execCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"exec\",\n\t\tShort: \"execute the command provided\",\n\t\tLong: `execute the command provided after the subcommand inside a span, measuring\nand reporting how long it took to run. The wrapping span's w3c traceparent is automatically\npassed to the child process's environment as TRACEPARENT.\n\nExamples:\n\notel-cli exec -n my-cool-thing -s interesting-step curl https://cool-service/api/v1/endpoint\n\notel-cli exec -s \"outer span\" -- otel-cli exec -s \"inner span\" sleep 1`,\n\t\tRun:  doExec,\n\t\tArgs: cobra.MinimumNArgs(1),\n\t}\n\n\taddCommonParams(&cmd, config)\n\taddSpanParams(&cmd, config)\n\taddAttrParams(&cmd, config)\n\taddClientParams(&cmd, config)\n\n\tdefaults := DefaultConfig()\n\tcmd.Flags().StringVar(\n\t\t&config.ExecCommandTimeout,\n\t\t\"command-timeout\",\n\t\tdefaults.ExecCommandTimeout,\n\t\t\"timeout for the child process, when 0 otel-cli will wait forever\",\n\t)\n\n\tcmd.Flags().BoolVar(\n\t\t&config.ExecTpDisableInject,\n\t\t\"tp-disable-inject\",\n\t\tdefaults.ExecTpDisableInject,\n\t\t\"disable automatically replacing {{traceparent}} with a traceparent\",\n\t)\n\n\treturn &cmd\n}\n\nfunc doExec(cmd *cobra.Command, args []string) {\n\tctx := cmd.Context()\n\tconfig := getConfig(ctx)\n\tspan := config.NewProtobufSpan()\n\tprocessAttrs := processArgAttrs(args) // might be overwritten in process setup\n\n\t// no deadline if there is no command timeout set\n\tcancelCtxDeadline := func() {}\n\t// fork the context for the command so its deadline doesn't impact the otlpclient ctx\n\tcmdCtx := ctx\n\tcmdTimeout := config.ParseExecCommandTimeout()\n\tif cmdTimeout > 0 {\n\t\tcmdCtx, cancelCtxDeadline = context.WithDeadline(ctx, time.Now().Add(cmdTimeout))\n\t}\n\n\t// pass the existing env but add the latest TRACEPARENT carrier so e.g.\n\t// otel-cli exec 'otel-cli exec sleep 1' will relate the spans automatically\n\tchildEnv := []string{}\n\n\t// set the traceparent to the current span to be available to the child process\n\tvar tp traceparent.Traceparent\n\tif config.GetIsRecording() {\n\t\ttp = otlpclient.TraceparentFromProtobufSpan(span, config.GetIsRecording())\n\t\tchildEnv = append(childEnv, fmt.Sprintf(\"TRACEPARENT=%s\", tp.Encode()))\n\t\t// when not recording, and a traceparent is available, pass it through\n\t} else if !config.TraceparentIgnoreEnv {\n\t\ttp := config.LoadTraceparent()\n\t\tif tp.Initialized {\n\t\t\tchildEnv = append(childEnv, fmt.Sprintf(\"TRACEPARENT=%s\", tp.Encode()))\n\t\t}\n\t}\n\n\tvar child *exec.Cmd\n\tif len(args) > 1 {\n\t\ttpArgs := make([]string, len(args)-1)\n\n\t\tif config.ExecTpDisableInject {\n\t\t\tcopy(tpArgs, args[1:])\n\t\t} else {\n\t\t\t// loop over the args replacing {{traceparent}} with the current tp\n\t\t\tfor i, arg := range args[1:] {\n\t\t\t\ttpArgs[i] = strings.Replace(arg, \"{{traceparent}}\", tp.Encode(), -1)\n\t\t\t}\n\n\t\t\t// overwrite process args attributes with the injected values\n\t\t\tprocessAttrs = processArgAttrs(append([]string{args[0]}, tpArgs...))\n\t\t}\n\n\t\tchild = exec.CommandContext(cmdCtx, args[0], tpArgs...)\n\t} else {\n\t\tchild = exec.CommandContext(cmdCtx, args[0])\n\t}\n\n\t// attach all stdio to the parent's handles\n\tchild.Stdin = os.Stdin\n\tchild.Stdout = os.Stdout\n\tchild.Stderr = os.Stderr\n\n\t// grab everything BUT the TRACEPARENT envvar\n\tfor _, env := range os.Environ() {\n\t\tif !strings.HasPrefix(env, \"TRACEPARENT=\") {\n\t\t\tchildEnv = append(childEnv, env)\n\t\t}\n\t}\n\tchild.Env = childEnv\n\n\t// ctrl-c (sigint) is forwarded to the child process\n\tsignals := make(chan os.Signal, 10)\n\tsignalsDone := make(chan struct{})\n\tsignal.Notify(signals, os.Interrupt)\n\tgo func() {\n\t\tsig := <-signals\n\t\tchild.Process.Signal(sig)\n\t\t// this might not seem necessary but without it, otel-cli exits before sending the span\n\t\tclose(signalsDone)\n\t}()\n\n\tspan.StartTimeUnixNano = uint64(time.Now().UnixNano())\n\tif err := child.Run(); err != nil {\n\t\tspan.Status = &tracev1.Status{\n\t\t\tMessage: fmt.Sprintf(\"exec command failed: %s\", err),\n\t\t\tCode:    tracev1.Status_STATUS_CODE_ERROR,\n\t\t}\n\t}\n\tspan.EndTimeUnixNano = uint64(time.Now().UnixNano())\n\n\t// append process attributes\n\tspan.Attributes = append(span.Attributes, processAttrs...)\n\tpidAttrs := processPidAttrs(config, int64(child.Process.Pid), int64(os.Getpid()))\n\tspan.Attributes = append(span.Attributes, pidAttrs...)\n\n\tcancelCtxDeadline()\n\tclose(signals)\n\t<-signalsDone\n\n\t// set --timeout on just the OTLP egress, starting now instead of process start time\n\tctx, cancelCtxDeadline = context.WithDeadline(ctx, time.Now().Add(config.GetTimeout()))\n\tdefer cancelCtxDeadline()\n\n\tctx, client := StartClient(ctx, config)\n\tctx, err := otlpclient.SendSpan(ctx, client, config, span)\n\tif err != nil {\n\t\tconfig.SoftFail(\"unable to send span: %s\", err)\n\t}\n\n\t_, err = client.Stop(ctx)\n\tif err != nil {\n\t\tconfig.SoftFail(\"client.Stop() failed: %s\", err)\n\t}\n\n\t// set the global exit code so main() can grab it and os.Exit() properly\n\tDiag.ExecExitCode = child.ProcessState.ExitCode()\n\n\tconfig.PropagateTraceparent(span, os.Stdout)\n}\n\n// processArgAttrs turns the provided args list into OTel attributes\n// that can be appended to a protobuf span's span.Attributes.\n// https://opentelemetry.io/docs/specs/semconv/attributes-registry/process/\nfunc processArgAttrs(args []string) []*commonpb.KeyValue {\n\t// convert args to an OpenTelemetry string list\n\tavlist := make([]*commonpb.AnyValue, len(args))\n\tfor i, v := range args {\n\t\tsv := commonpb.AnyValue_StringValue{StringValue: v}\n\t\tav := commonpb.AnyValue{Value: &sv}\n\t\tavlist[i] = &av\n\t}\n\n\treturn []*commonpb.KeyValue{\n\t\t{\n\t\t\tKey: \"process.command\",\n\t\t\tValue: &commonpb.AnyValue{\n\t\t\t\tValue: &commonpb.AnyValue_StringValue{StringValue: args[0]},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKey: \"process.command_args\",\n\t\t\tValue: &commonpb.AnyValue{\n\t\t\t\tValue: &commonpb.AnyValue_ArrayValue{\n\t\t\t\t\tArrayValue: &commonpb.ArrayValue{\n\t\t\t\t\t\tValues: avlist,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// processPidAttrs returns process.{owner,pid,parent_pid} attributes ready\n// to append to a protobuf span's span.Attributes.\nfunc processPidAttrs(config Config, ppid, pid int64) []*commonpb.KeyValue {\n\tuser, err := user.Current()\n\tconfig.SoftLogIfErr(err)\n\n\treturn []*commonpb.KeyValue{\n\t\t{\n\t\t\tKey: \"process.owner\",\n\t\t\tValue: &commonpb.AnyValue{\n\t\t\t\tValue: &commonpb.AnyValue_StringValue{StringValue: user.Username},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKey: \"process.pid\",\n\t\t\tValue: &commonpb.AnyValue{\n\t\t\t\tValue: &commonpb.AnyValue_IntValue{IntValue: pid},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKey: \"process.parent_pid\",\n\t\t\tValue: &commonpb.AnyValue{\n\t\t\t\tValue: &commonpb.AnyValue_IntValue{IntValue: ppid},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "otelcli/otlpclient.go",
    "content": "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 uses the Config to setup and start either a gRPC or HTTP client,\n// and returns the OTLPClient interface to them.\nfunc StartClient(ctx context.Context, config Config) (context.Context, otlpclient.OTLPClient) {\n\tif !config.GetIsRecording() {\n\t\treturn ctx, otlpclient.NewNullClient(config)\n\t}\n\n\tif config.Protocol != \"\" && config.Protocol != \"grpc\" && config.Protocol != \"http/protobuf\" {\n\t\terr := fmt.Errorf(\"invalid protocol setting %q\", config.Protocol)\n\t\tDiag.Error = err.Error()\n\t\tconfig.SoftFail(err.Error())\n\t}\n\n\tendpointURL := config.GetEndpoint()\n\n\tvar client otlpclient.OTLPClient\n\tif config.Protocol != \"grpc\" &&\n\t\t(strings.HasPrefix(config.Protocol, \"http/\") ||\n\t\t\tendpointURL.Scheme == \"http\" ||\n\t\t\tendpointURL.Scheme == \"https\") {\n\t\tclient = otlpclient.NewHttpClient(config)\n\t} else {\n\t\tclient = otlpclient.NewGrpcClient(config)\n\t}\n\n\tctx, err := client.Start(ctx)\n\tif err != nil {\n\t\tDiag.Error = err.Error()\n\t\tconfig.SoftFail(\"Failed to start OTLP client: %s\", err)\n\t}\n\n\treturn ctx, client\n}\n"
  },
  {
    "path": "otelcli/root.go",
    "content": "// Package otelcli implements the otel-cli subcommands and argument parsing\n// with Cobra and implements functionality using otlpclient and otlpserver.\npackage otelcli\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// cliContextKey is a type for storing an Config in context.\ntype cliContextKey string\n\n// configContextKey returns the typed key for storing/retrieving config in context.\nfunc configContextKey() cliContextKey {\n\treturn cliContextKey(\"config\")\n}\n\n// getConfigRef retrieves the otelcli.Config from the context and returns a\n// pointer to it.\nfunc getConfigRef(ctx context.Context) *Config {\n\tif cv := ctx.Value(configContextKey()); cv != nil {\n\t\tif c, ok := cv.(*Config); ok {\n\t\t\treturn c\n\t\t} else {\n\t\t\tpanic(\"BUG: failed to unwrap config that was in context, please report an issue\")\n\t\t}\n\t} else {\n\t\tpanic(\"BUG: failed to retrieve config from context, please report an issue\")\n\t}\n}\n\n// getConfig retrieves the otelcli.Config from context and returns a copy.\nfunc getConfig(ctx context.Context) Config {\n\tconfig := getConfigRef(ctx)\n\treturn *config\n}\n\n// createRootCmd builds up the Cobra command-line, calling through to subcommand\n// builder funcs to build the whole tree.\nfunc createRootCmd(config *Config) *cobra.Command {\n\t// rootCmd represents the base command when called without any subcommands\n\tvar rootCmd = &cobra.Command{\n\t\tUse:   \"otel-cli\",\n\t\tShort: \"CLI for creating and sending OpenTelemetry spans and events.\",\n\t\tLong:  `A command-line interface for generating OpenTelemetry data on the command line.`,\n\t\tPersistentPreRun: func(cmd *cobra.Command, args []string) {\n\t\t\tconfig := getConfigRef(cmd.Context())\n\t\t\tif err := config.LoadFile(); err != nil {\n\t\t\t\tconfig.SoftFail(\"Error while loading configuration file %s: %s\", config.CfgFile, err)\n\t\t\t}\n\t\t\tif err := config.LoadEnv(os.Getenv); err != nil {\n\t\t\t\t// will need to specify --fail --verbose flags to see these errors\n\t\t\t\tconfig.SoftFail(\"Error while loading environment variables: %s\", err)\n\t\t\t}\n\t\t},\n\t}\n\n\tcobra.EnableCommandSorting = false\n\trootCmd.Flags().SortFlags = false\n\n\tDiag.NumArgs = len(os.Args) - 1\n\tDiag.CliArgs = []string{}\n\tif len(os.Args) > 1 {\n\t\tDiag.CliArgs = os.Args[1:]\n\t}\n\n\t// add all the subcommands to rootCmd\n\trootCmd.AddCommand(spanCmd(config))\n\trootCmd.AddCommand(execCmd(config))\n\trootCmd.AddCommand(statusCmd(config))\n\trootCmd.AddCommand(serverCmd(config))\n\trootCmd.AddCommand(versionCmd(config))\n\trootCmd.AddCommand(completionCmd(config))\n\n\treturn rootCmd\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once.\nfunc Execute(version string) {\n\tconfig := DefaultConfig()\n\tconfig.Version = version\n\n\t// Cobra can tunnel config through context, so set that up now\n\tctx := context.WithValue(context.Background(), configContextKey(), &config)\n\n\trootCmd := createRootCmd(&config)\n\tcobra.CheckErr(rootCmd.ExecuteContext(ctx))\n}\n\n// addCommonParams adds the --config and --endpoint params to the command.\nfunc addCommonParams(cmd *cobra.Command, config *Config) {\n\tdefaults := DefaultConfig()\n\n\t// --config / -c a JSON configuration file\n\tcmd.Flags().StringVarP(&config.CfgFile, \"config\", \"c\", defaults.CfgFile, \"JSON configuration file\")\n\t// --endpoint an endpoint to send otlp output to\n\tcmd.Flags().StringVar(&config.Endpoint, \"endpoint\", defaults.Endpoint, \"host and port for the desired OTLP/gRPC or OTLP/HTTP endpoint (use http:// or https:// for OTLP/HTTP)\")\n\t// --traces-endpoint sets the endpoint for the traces signal\n\tcmd.Flags().StringVar(&config.TracesEndpoint, \"traces-endpoint\", defaults.TracesEndpoint, \"HTTP(s) URL for traces\")\n\t// --protocol allows setting the OTLP protocol instead of relying on auto-detection from URI\n\tcmd.Flags().StringVar(&config.Protocol, \"protocol\", defaults.Protocol, \"desired OTLP protocol: grpc or http/protobuf\")\n\t// --timeout a default timeout to use in all otel-cli operations (default 1s)\n\tcmd.Flags().StringVar(&config.Timeout, \"timeout\", defaults.Timeout, \"timeout for otel-cli operations, all timeouts in otel-cli use this value\")\n\t// --verbose tells otel-cli to actually log errors to stderr instead of failing silently\n\tcmd.Flags().BoolVar(&config.Verbose, \"verbose\", defaults.Verbose, \"print errors on failure instead of always being silent\")\n\t// --fail causes a non-zero exit status on error\n\tcmd.Flags().BoolVar(&config.Fail, \"fail\", defaults.Fail, \"on failure, exit with a non-zero status\")\n}\n\n// addClientParams adds the common CLI flags for e.g. span and exec to the command.\n// envvars are named according to the otel specs, others use the OTEL_CLI prefix\n// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md\n// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md\nfunc addClientParams(cmd *cobra.Command, config *Config) {\n\tdefaults := DefaultConfig()\n\tconfig.Headers = make(map[string]string)\n\n\t// OTEL_EXPORTER standard env and variable params\n\tcmd.Flags().StringToStringVar(&config.Headers, \"otlp-headers\", defaults.Headers, \"a comma-sparated list of key=value headers to send on OTLP connection\")\n\n\t// DEPRECATED\n\t// TODO: remove before 1.0\n\tcmd.Flags().BoolVar(&config.Blocking, \"otlp-blocking\", defaults.Blocking, \"DEPRECATED: does nothing, please file an issue if you need this.\")\n\n\tcmd.Flags().BoolVar(&config.Insecure, \"insecure\", defaults.Insecure, \"allow connecting to cleartext endpoints\")\n\tcmd.Flags().StringVar(&config.TlsCACert, \"tls-ca-cert\", defaults.TlsCACert, \"a file containing the certificate authority bundle\")\n\tcmd.Flags().StringVar(&config.TlsClientCert, \"tls-client-cert\", defaults.TlsClientCert, \"a file containing the client certificate\")\n\tcmd.Flags().StringVar(&config.TlsClientKey, \"tls-client-key\", defaults.TlsClientKey, \"a file containing the client certificate key\")\n\tcmd.Flags().BoolVar(&config.TlsNoVerify, \"tls-no-verify\", defaults.TlsNoVerify, \"insecure! disables verification of the server certificate and name, mostly for self-signed CAs\")\n\t// --no-tls-verify is deprecated, will remove before 1.0\n\tcmd.Flags().BoolVar(&config.TlsNoVerify, \"no-tls-verify\", defaults.TlsNoVerify, \"(deprecated) same as --tls-no-verify\")\n\n\t// OTEL_CLI trace propagation options\n\tcmd.Flags().BoolVar(&config.TraceparentRequired, \"tp-required\", defaults.TraceparentRequired, \"when set to true, fail and log if a traceparent can't be picked up from TRACEPARENT ennvar or a carrier file\")\n\tcmd.Flags().StringVar(&config.TraceparentCarrierFile, \"tp-carrier\", defaults.TraceparentCarrierFile, \"a file for reading and WRITING traceparent across invocations\")\n\tcmd.Flags().BoolVar(&config.TraceparentIgnoreEnv, \"tp-ignore-env\", defaults.TraceparentIgnoreEnv, \"ignore the TRACEPARENT envvar even if it's set\")\n\tcmd.Flags().BoolVar(&config.TraceparentPrint, \"tp-print\", defaults.TraceparentPrint, \"print the trace id, span id, and the w3c-formatted traceparent representation of the new span\")\n\tcmd.Flags().BoolVarP(&config.TraceparentPrintExport, \"tp-export\", \"p\", defaults.TraceparentPrintExport, \"same as --tp-print but it puts an 'export ' in front so it's more convinenient to source in scripts\")\n}\n\nfunc addSpanParams(cmd *cobra.Command, config *Config) {\n\tdefaults := DefaultConfig()\n\n\t// --name / -s\n\tcmd.Flags().StringVarP(&config.SpanName, \"name\", \"n\", defaults.SpanName, \"set the name of the span\")\n\t// --service / -n\n\tcmd.Flags().StringVarP(&config.ServiceName, \"service\", \"s\", defaults.ServiceName, \"set the name of the application sent on the traces\")\n\t// --kind / -k\n\tcmd.Flags().StringVarP(&config.Kind, \"kind\", \"k\", defaults.Kind, \"set the trace kind, e.g. internal, server, client, producer, consumer\")\n\n\t// expert options: --force-trace-id, --force-span-id, --force-parent-span-id allow setting custom trace, span and parent span ids\n\tcmd.Flags().StringVar(&config.ForceTraceId, \"force-trace-id\", defaults.ForceTraceId, \"expert: force the trace id to be the one provided in hex\")\n\tcmd.Flags().StringVar(&config.ForceSpanId, \"force-span-id\", defaults.ForceSpanId, \"expert: force the span id to be the one provided in hex\")\n\tcmd.Flags().StringVar(&config.ForceParentSpanId, \"force-parent-span-id\", defaults.ForceParentSpanId, \"expert: force the parent span id to be the one provided in hex\")\n\n\taddSpanStatusParams(cmd, config)\n}\n\nfunc addSpanStartEndParams(cmd *cobra.Command, config *Config) {\n\tdefaults := DefaultConfig()\n\n\t// --start $timestamp (RFC3339 or Unix_Epoch.Nanos)\n\tcmd.Flags().StringVar(&config.SpanStartTime, \"start\", defaults.SpanStartTime, \"a Unix epoch or RFC3339 timestamp for the start of the span\")\n\n\t// --end $timestamp\n\tcmd.Flags().StringVar(&config.SpanEndTime, \"end\", defaults.SpanEndTime, \"an Unix epoch or RFC3339 timestamp for the end of the span\")\n}\n\nfunc addSpanStatusParams(cmd *cobra.Command, config *Config) {\n\tdefaults := DefaultConfig()\n\n\t// --status-code / -sc\n\tcmd.Flags().StringVar(&config.StatusCode, \"status-code\", defaults.StatusCode, \"set the span status code, e.g. unset|ok|error\")\n\t// --status-description / -sd\n\tcmd.Flags().StringVar(&config.StatusDescription, \"status-description\", defaults.StatusDescription, \"set the span status description when a span status code of error is set, e.g. 'cancelled'\")\n}\n\nfunc addAttrParams(cmd *cobra.Command, config *Config) {\n\tdefaults := DefaultConfig()\n\t// --attrs key=value,foo=bar\n\tconfig.Attributes = make(map[string]string)\n\tcmd.Flags().StringToStringVarP(&config.Attributes, \"attrs\", \"a\", defaults.Attributes, \"a comma-separated list of key=value attributes\")\n}\n"
  },
  {
    "path": "otelcli/server.go",
    "content": "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 defaultOtlpEndpoint = \"grpc://localhost:4317\"\nconst spanBgSockfilename = \"otel-cli-background.sock\"\n\nfunc serverCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"server\",\n\t\tShort: \"run an embedded OTLP server\",\n\t\tLong:  \"Run otel-cli as an OTLP server. See subcommands.\",\n\t}\n\n\tcmd.AddCommand(serverJsonCmd(config))\n\tcmd.AddCommand(serverTuiCmd(config))\n\n\treturn &cmd\n}\n\n// runServer runs the server on either grpc or http and blocks until the server\n// stops or is killed.\nfunc runServer(config Config, cb otlpserver.Callback, stop otlpserver.Stopper) {\n\t// unlike the rest of otel-cli, server should default to localhost:4317\n\tif config.Endpoint == \"\" {\n\t\tconfig.Endpoint = defaultOtlpEndpoint\n\t}\n\tendpointURL, _ := config.ParseEndpoint()\n\n\tvar cs otlpserver.OtlpServer\n\tif config.Protocol != \"grpc\" &&\n\t\t(strings.HasPrefix(config.Protocol, \"http/\") ||\n\t\t\tendpointURL.Scheme == \"http\") {\n\t\tcs = otlpserver.NewServer(\"http\", cb, stop)\n\t} else if config.Protocol == \"https\" || endpointURL.Scheme == \"https\" {\n\t\tconfig.SoftFail(\"https server is not supported yet, please raise an issue\")\n\t} else {\n\t\tcs = otlpserver.NewServer(\"grpc\", cb, stop)\n\t}\n\n\tdefer cs.Stop()\n\tcs.ListenAndServe(endpointURL.Host)\n}\n"
  },
  {
    "path": "otelcli/server_json.go",
    "content": "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\t\"github.com/equinix-labs/otel-cli/otlpserver\"\n\t\"github.com/spf13/cobra\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// jsonSvr holds the command-line configured settings for otel-cli server json\nvar jsonSvr struct {\n\toutDir    string\n\tstdout    bool\n\tmaxSpans  int\n\tspansSeen int\n}\n\nfunc serverJsonCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"json\",\n\t\tShort: \"write spans to json or stdout\",\n\t\tLong:  \"\",\n\t\tRun:   doServerJson,\n\t}\n\n\taddCommonParams(&cmd, config)\n\tcmd.Flags().StringVar(&jsonSvr.outDir, \"dir\", \"\", \"write spans to json in the specified directory\")\n\tcmd.Flags().BoolVar(&jsonSvr.stdout, \"stdout\", false, \"write span jsons to stdout\")\n\tcmd.Flags().IntVar(&jsonSvr.maxSpans, \"max-spans\", 0, \"exit the server after this many spans come in\")\n\n\treturn &cmd\n}\n\nfunc doServerJson(cmd *cobra.Command, args []string) {\n\tconfig := getConfig(cmd.Context())\n\tstop := func(otlpserver.OtlpServer) {}\n\tcs := otlpserver.NewGrpcServer(renderJson, stop)\n\n\t// stops the grpc server after timeout\n\ttimeout := config.ParseCliTimeout()\n\tif timeout > 0 {\n\t\tgo func() {\n\t\t\ttime.Sleep(timeout)\n\t\t\tcs.Stop()\n\t\t}()\n\t}\n\n\trunServer(config, renderJson, stop)\n}\n\n// writeFile takes the spans and events and writes them out to json files in the\n// tid/sid/span.json and tid/sid/events.json files.\nfunc renderJson(ctx context.Context, span *tracepb.Span, events []*tracepb.Span_Event, ss *tracepb.ResourceSpans, headers map[string]string, meta map[string]string) bool {\n\tjsonSvr.spansSeen++ // count spans for exiting on --max-spans\n\n\t// TODO: check for existence of outdir and error when it doesn't exist\n\tvar outpath string\n\tif jsonSvr.outDir != \"\" {\n\t\t// create trace directory\n\t\toutpath = filepath.Join(jsonSvr.outDir, hex.EncodeToString(span.TraceId))\n\t\tos.Mkdir(outpath, 0755) // ignore errors for now\n\n\t\t// create span directory\n\t\toutpath = filepath.Join(outpath, hex.EncodeToString(span.SpanId))\n\t\tos.Mkdir(outpath, 0755) // ignore errors for now\n\t}\n\n\t// write span to file\n\t// TODO: if a span comes in twice should we continue to overwrite span.json\n\t// or attempt some kind of merge? (e.g. of attributes)\n\tsjs, err := json.Marshal(span)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to marshal span to json: %s\", err)\n\t}\n\n\t// write the span to /path/tid/sid/span.json\n\twriteJson(outpath, \"span.json\", sjs)\n\n\t// only write events out if there is at least one\n\tfor i, e := range events {\n\t\tejs, err := json.Marshal(e)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to marshal span event to json: %s\", err)\n\t\t}\n\n\t\t// write events to /path/tid/sid/event-%d.json\n\t\t// TODO: ordering might be a problem if people rely on it...\n\t\tfilename := \"event-\" + strconv.Itoa(i) + \".json\"\n\t\twriteJson(outpath, filename, ejs)\n\t}\n\n\tif jsonSvr.maxSpans > 0 && jsonSvr.spansSeen >= jsonSvr.maxSpans {\n\t\treturn true // will cause the server loop to exit\n\t}\n\n\treturn false\n}\n\n// writeJson takes a directory path, a filename, and json. When the path is not empty\n// string the json is written to path/filename. If --stdout was specified the json will\n// be printed as a line to stdout.\nfunc writeJson(path, filename string, js []byte) {\n\tif path != \"\" {\n\t\tspanfile := filepath.Join(path, filename)\n\t\terr := os.WriteFile(spanfile, js, 0644)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"could not write to file %q: %s\", spanfile, err)\n\t\t}\n\t}\n\n\tif jsonSvr.stdout {\n\t\tos.Stdout.Write(js)\n\t\tos.Stdout.WriteString(\"\\n\")\n\t}\n}\n"
  },
  {
    "path": "otelcli/server_tui.go",
    "content": "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-cli/otlpclient\"\n\t\"github.com/equinix-labs/otel-cli/otlpserver\"\n\t\"github.com/pterm/pterm\"\n\t\"github.com/spf13/cobra\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\nvar tuiServer struct {\n\tlines  SpanEventUnionList\n\ttraces map[string]*tracepb.Span // for looking up top span of trace by trace id\n\tarea   *pterm.AreaPrinter\n}\n\nfunc serverTuiCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"tui\",\n\t\tShort: \"display spans in a terminal UI\",\n\t\tLong: `Run otel-cli as an OTLP server with a terminal UI that displays traces.\n\t\n\t# run otel-cli as a local server and print spans to the console as a table\n\totel-cli server tui`,\n\t\tRun: doServerTui,\n\t}\n\n\taddCommonParams(&cmd, config)\n\treturn &cmd\n}\n\n// doServerTui implements the 'otel-cli server tui' subcommand.\nfunc doServerTui(cmd *cobra.Command, args []string) {\n\tconfig := getConfig(cmd.Context())\n\tarea, err := pterm.DefaultArea.Start()\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to set up terminal for rendering: %s\", err)\n\t}\n\ttuiServer.area = area\n\n\ttuiServer.lines = []SpanEventUnion{}\n\ttuiServer.traces = make(map[string]*tracepb.Span)\n\n\tstop := func(otlpserver.OtlpServer) {\n\t\ttuiServer.area.Stop()\n\t}\n\n\trunServer(config, renderTui, stop)\n}\n\n// renderTui takes the given span and events, appends them to the in-memory\n// event list, sorts that, then prints it as a pterm table.\nfunc renderTui(ctx context.Context, span *tracepb.Span, events []*tracepb.Span_Event, rss *tracepb.ResourceSpans, headers map[string]string, meta map[string]string) bool {\n\tspanTraceId := hex.EncodeToString(span.TraceId)\n\tif _, ok := tuiServer.traces[spanTraceId]; !ok {\n\t\ttuiServer.traces[spanTraceId] = span\n\t}\n\n\ttuiServer.lines = append(tuiServer.lines, SpanEventUnion{Span: span})\n\tfor _, e := range events {\n\t\ttuiServer.lines = append(tuiServer.lines, SpanEventUnion{Span: span, Event: e})\n\t}\n\tsort.Sort(tuiServer.lines)\n\ttrimTuiEvents()\n\n\ttd := pterm.TableData{\n\t\t{\"Trace ID\", \"Span ID\", \"Parent\", \"Name\", \"Kind\", \"Start\", \"End\", \"Elapsed\"},\n\t}\n\n\tfor _, line := range tuiServer.lines {\n\t\tvar traceId, spanId, parent, name, kind string\n\t\tvar startOffset, endOffset, elapsed int64\n\t\tif line.IsSpan() {\n\t\t\tname = line.Span.Name\n\t\t\tkind = otlpclient.SpanKindIntToString(line.Span.GetKind())\n\t\t\ttraceId = line.TraceIdString()\n\t\t\tspanId = line.SpanIdString()\n\n\t\t\tif tspan, ok := tuiServer.traces[traceId]; ok {\n\t\t\t\tstartOffset = roundedDelta(line.Span.StartTimeUnixNano, tspan.StartTimeUnixNano)\n\t\t\t\tendOffset = roundedDelta(line.Span.EndTimeUnixNano, tspan.StartTimeUnixNano)\n\t\t\t} else {\n\t\t\t\tendOffset = roundedDelta(line.Span.EndTimeUnixNano, line.Span.StartTimeUnixNano)\n\t\t\t}\n\n\t\t\tif len(line.Span.ParentSpanId) > 0 {\n\t\t\t\ttraceId = \"\" // hide it after printing the first trace id\n\t\t\t\tparent = hex.EncodeToString(line.Span.ParentSpanId)\n\t\t\t}\n\n\t\t\telapsed = endOffset - startOffset\n\t\t} else { // span events\n\t\t\tname = line.Event.Name\n\t\t\tkind = \"event\"\n\t\t\ttraceId = \"\" // hide ids on events to make screen less busy\n\t\t\tparent = line.SpanIdString()\n\t\t\tif tspan, ok := tuiServer.traces[traceId]; ok {\n\t\t\t\tstartOffset = roundedDelta(line.Event.TimeUnixNano, tspan.StartTimeUnixNano)\n\t\t\t} else {\n\t\t\t\tstartOffset = roundedDelta(line.Event.TimeUnixNano, line.Span.StartTimeUnixNano)\n\t\t\t}\n\t\t\tendOffset = startOffset\n\t\t\telapsed = 0\n\t\t}\n\n\t\ttd = append(td, []string{\n\t\t\ttraceId,\n\t\t\tspanId,\n\t\t\tparent,\n\t\t\tname,\n\t\t\tkind,\n\t\t\tstrconv.FormatInt(startOffset, 10),\n\t\t\tstrconv.FormatInt(endOffset, 10),\n\t\t\tstrconv.FormatInt(elapsed, 10),\n\t\t})\n\t}\n\n\ttuiServer.area.Update(pterm.DefaultTable.WithHasHeader().WithData(td).Srender())\n\treturn false // keep running until user hits ctrl-c\n}\n\n// roundedDelta takes to uint64 nanos values, cuts them down to milliseconds,\n// takes the delta (absolute value, so any order is fine), and returns an int64\n// of ms between the values.\nfunc roundedDelta(ts1, ts2 uint64) int64 {\n\tdeltaMs := math.Abs(float64(ts1/1000000) - float64(ts2/1000000))\n\trounded := math.Round(deltaMs)\n\treturn int64(rounded)\n}\n\n// trimEvents looks to see if there's room on the screen for the number of incoming\n// events and removes the oldest traces until there's room\n// TODO: how to hand really huge traces that would scroll off the screen entirely?\nfunc trimTuiEvents() {\n\tmaxRows := pterm.GetTerminalHeight() // TODO: allow override of this?\n\n\tif len(tuiServer.lines) == 0 || len(tuiServer.lines) < maxRows {\n\t\treturn // plenty of room, nothing to do\n\t}\n\n\tend := len(tuiServer.lines) - 1              // should never happen but default to all\n\tneed := (len(tuiServer.lines) - maxRows) + 2 // trim at least this many\n\ttid := tuiServer.lines[0].TraceIdString()    // we always remove the whole trace\n\tfor i, v := range tuiServer.lines {\n\t\tif v.TraceIdString() == tid {\n\t\t\tend = i\n\t\t} else {\n\t\t\tif end+1 < need {\n\t\t\t\t// trace id changed, advance the trim point, and change trace ids\n\t\t\t\ttid = v.TraceIdString()\n\t\t\t\tend = i\n\t\t\t} else {\n\t\t\t\tbreak // made enough room, we can quit early\n\t\t\t}\n\t\t}\n\t}\n\n\t// might need to realloc to not leak memory here?\n\ttuiServer.lines = tuiServer.lines[end:]\n}\n\n// SpanEventUnion is for server_tui so it can sort spans and events together\n// by timestamp.\ntype SpanEventUnion struct {\n\tSpan  *tracepb.Span\n\tEvent *tracepb.Span_Event\n}\n\nfunc (seu *SpanEventUnion) TraceIdString() string { return hex.EncodeToString(seu.Span.TraceId) }\nfunc (seu *SpanEventUnion) SpanIdString() string  { return hex.EncodeToString(seu.Span.SpanId) }\n\nfunc (seu *SpanEventUnion) UnixNanos() uint64 {\n\tif seu.IsSpan() {\n\t\treturn seu.Span.StartTimeUnixNano\n\t} else {\n\t\treturn seu.Event.TimeUnixNano\n\t}\n}\n\n// IsSpan returns true if this union is for an event. Span is always populated\n// but Event is only populated for events.\nfunc (seu *SpanEventUnion) IsSpan() bool { return seu.Event == nil }\n\n// SpanEventUnionList is a sortable list of SpanEventUnion, sorted on timestamp.\ntype SpanEventUnionList []SpanEventUnion\n\nfunc (sl SpanEventUnionList) Len() int           { return len(sl) }\nfunc (sl SpanEventUnionList) Swap(i, j int)      { sl[i], sl[j] = sl[j], sl[i] }\nfunc (sl SpanEventUnionList) Less(i, j int) bool { return sl[i].UnixNanos() < sl[j].UnixNanos() }\n"
  },
  {
    "path": "otelcli/span.go",
    "content": "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/cobra\"\n)\n\n// spanCmd represents the span command\nfunc spanCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"span\",\n\t\tShort: \"create an OpenTelemetry span and send it\",\n\t\tLong: `Create an OpenTelemetry span as specified and send it along. The\nspan can be customized with a start/end time via RFC3339 or Unix epoch format,\nwith support for nanoseconds on both.\n\nExample:\n\totel-cli span \\\n\t\t--service \"my-application\" \\\n\t\t--name \"send data to the server\" \\\n\t\t--start 2021-03-24T07:28:05.12345Z \\\n\t\t--end $(date +%s.%N) \\\n\t\t--attrs \"os.kernel=$(uname -r)\" \\\n\t\t--tp-print\n`,\n\t\tRun: doSpan,\n\t}\n\n\tcmd.Flags().SortFlags = false\n\n\taddCommonParams(&cmd, config)\n\taddSpanParams(&cmd, config)\n\taddSpanStartEndParams(&cmd, config)\n\taddAttrParams(&cmd, config)\n\taddClientParams(&cmd, config)\n\n\t// subcommands\n\tcmd.AddCommand(spanBgCmd(config))\n\tcmd.AddCommand(spanEventCmd(config))\n\tcmd.AddCommand(spanEndCmd(config))\n\n\treturn &cmd\n}\n\nfunc doSpan(cmd *cobra.Command, args []string) {\n\tctx := cmd.Context()\n\tconfig := getConfig(ctx)\n\tctx, cancel := context.WithDeadline(ctx, time.Now().Add(config.GetTimeout()))\n\tdefer cancel()\n\tctx, client := StartClient(ctx, config)\n\tspan := config.NewProtobufSpan()\n\tctx, err := otlpclient.SendSpan(ctx, client, config, span)\n\tconfig.SoftFailIfErr(err)\n\t_, err = client.Stop(ctx)\n\tconfig.SoftFailIfErr(err)\n\tconfig.PropagateTraceparent(span, os.Stdout)\n}\n"
  },
  {
    "path": "otelcli/span_background.go",
    "content": "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/otel-cli/otlpclient\"\n\t\"github.com/spf13/cobra\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// spanBgCmd represents the span background command\nfunc spanBgCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"background\",\n\t\tShort: \"create background span handler\",\n\t\tLong: `Creates a background span handler that listens on a Unix socket\nso you can add events to it. The span is closed when the process exits from\ntimeout, (catchable) signals, or deliberate exit.\n\n    socket_dir=$(mktemp -d)\n\totel-cli span background \\\n\t\t--service \"my-long-script.sh\" \\\n\t\t--name \"run the script\" \\\n\t\t--attrs \"os.kernel=$(uname -r)\" \\\n\t\t--timeout 60 \\\n\t\t--sockdir $socket_dir & # <-- notice the &\n\t\n\totel-cli span event \\\n\t\t--sockdir $socket_dir \\\n\t\t--name \"something interesting happened!\" \\\n\t\t--attrs \"foo=bar\"\n`,\n\t\tRun: doSpanBackground,\n\t}\n\n\tdefaults := DefaultConfig()\n\tcmd.Flags().SortFlags = false // don't sort subcommands\n\n\t// it seems like the socket should be required for background but it's\n\t// only necessary for adding events to the span. it should be fine to\n\t// start a background span at the top of a script then let it fall off\n\t// at the end to get an easy span\n\tcmd.Flags().StringVar(&config.BackgroundSockdir, \"sockdir\", defaults.BackgroundSockdir, \"a directory where a socket can be placed safely\")\n\n\tcmd.Flags().IntVar(&config.BackgroundParentPollMs, \"parent-poll\", defaults.BackgroundParentPollMs, \"number of milliseconds to wait between checking for whether the parent process exited\")\n\tcmd.Flags().BoolVar(&config.BackgroundWait, \"wait\", defaults.BackgroundWait, \"wait for background to be fully started and then return\")\n\tcmd.Flags().BoolVar(&config.BackgroundSkipParentPidCheck, \"skip-pid-check\", defaults.BackgroundSkipParentPidCheck, \"disable checking parent pid\")\n\n\taddCommonParams(&cmd, config)\n\taddSpanParams(&cmd, config)\n\taddClientParams(&cmd, config)\n\taddAttrParams(&cmd, config)\n\n\treturn &cmd\n}\n\nfunc doSpanBackground(cmd *cobra.Command, args []string) {\n\tctx := cmd.Context()\n\tconfig := getConfig(ctx)\n\tstarted := time.Now()\n\tctx, client := StartClient(ctx, config)\n\n\t// special case --wait, createBgClient() will wait for the socket to show up\n\t// then connect and send a no-op RPC. by this time e.g. --tp-carrier should\n\t// be all done and everything is ready to go without race conditions\n\tif config.BackgroundWait {\n\t\tclient, shutdown := createBgClient(config)\n\t\tdefer shutdown()\n\t\terr := client.Call(\"BgSpan.Wait\", &struct{}{}, &struct{}{})\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"error while waiting on span background: %s\", err)\n\t\t}\n\t\treturn\n\t}\n\n\tspan := config.NewProtobufSpan()\n\n\t// span background is a bit different from span/exec in that it might be\n\t// hanging out while other spans are created, so it does the traceparent\n\t// propagation before the server starts, instead of after\n\tconfig.PropagateTraceparent(span, os.Stdout)\n\n\tsockfile := path.Join(config.BackgroundSockdir, spanBgSockfilename)\n\tbgs := createBgServer(ctx, sockfile, span)\n\n\t// set up signal handlers to cleanly exit on SIGINT/SIGTERM etc\n\tsignals := make(chan os.Signal, 1)\n\tsignal.Notify(signals, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\t<-signals\n\t\tbgs.Shutdown()\n\t}()\n\n\t// in order to exit at the end of scripts this program needs a way to know\n\t// when the parent is gone. the most straightforward approach that should\n\t// be fine on most Unix-ish operating systems is to poll getppid and exit\n\t// when the parent process pid changes\n\tif !config.BackgroundSkipParentPidCheck {\n\t\tppid := os.Getppid()\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\ttime.Sleep(time.Duration(config.BackgroundParentPollMs) * time.Millisecond)\n\n\t\t\t\t// check if the parent process has changed, exit when it does\n\t\t\t\tcppid := os.Getppid()\n\t\t\t\tif cppid != ppid {\n\t\t\t\t\trt := time.Since(started)\n\t\t\t\t\tspanBgEndEvent(ctx, span, \"parent_exited\", rt)\n\t\t\t\t\tbgs.Shutdown()\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\t// start the timeout goroutine, this is a little late but the server\n\t// has to be up for this to make much sense\n\tif timeout := config.ParseCliTimeout(); timeout > 0 {\n\t\tgo func() {\n\t\t\ttime.Sleep(timeout)\n\t\t\trt := time.Since(started)\n\t\t\tspanBgEndEvent(ctx, span, \"timeout\", rt)\n\t\t\tbgs.Shutdown()\n\t\t}()\n\t}\n\n\t// will block until bgs.Shutdown()\n\tbgs.Run()\n\n\tspan.EndTimeUnixNano = uint64(time.Now().UnixNano())\n\n\tctx, cancel := context.WithDeadline(ctx, time.Now().Add(config.GetTimeout()))\n\tdefer cancel()\n\n\t_, err := otlpclient.SendSpan(ctx, client, config, span)\n\tif err != nil {\n\t\tconfig.SoftFail(\"Sending span failed: %s\", err)\n\t}\n}\n\n// spanBgEndEvent adds an event with the provided name, to the provided span\n// with uptime.milliseconds and timeout.seconds attributes.\nfunc spanBgEndEvent(ctx context.Context, span *tracepb.Span, name string, elapsed time.Duration) {\n\tconfig := getConfig(ctx)\n\tevent := otlpclient.NewProtobufSpanEvent()\n\tevent.Name = name\n\tevent.Attributes = otlpclient.StringMapAttrsToProtobuf(map[string]string{\n\t\t\"config.timeout\":      config.Timeout,\n\t\t\"otel-cli.runtime_ms\": strconv.FormatInt(elapsed.Milliseconds(), 10),\n\t})\n\n\tspan.Events = append(span.Events, event)\n}\n"
  },
  {
    "path": "otelcli/span_background_server.go",
    "content": "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\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/otlpclient\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// BgSpan is what is returned to all RPC clients and its methods are exported.\ntype BgSpan struct {\n\tTraceID     string `json:\"trace_id\"`\n\tSpanID      string `json:\"span_id\"`\n\tTraceparent string `json:\"traceparent\"`\n\tError       string `json:\"error\"`\n\tconfig      Config\n\tspan        *tracepb.Span\n\tshutdown    func()\n}\n\n// BgSpanEvent is a span event that the client will send.\ntype BgSpanEvent struct {\n\tName       string `json:\"name\"`\n\tTimestamp  string `json:\"timestamp\"`\n\tAttributes map[string]string\n}\n\n// BgEnd is an empty struct that can be sent to call End().\ntype BgEnd struct {\n\tAttributes map[string]string `json:\"span_attributes\" env:\"OTEL_CLI_ATTRIBUTES\"`\n\tStatusCode string            `json:\"status_code\"`\n\tStatusDesc string            `json:\"status_description\"`\n}\n\n// AddEvent takes a BgSpanEvent from the client and attaches an event to the span.\nfunc (bs BgSpan) AddEvent(bse *BgSpanEvent, reply *BgSpan) error {\n\treply.TraceID = hex.EncodeToString(bs.span.TraceId)\n\treply.SpanID = hex.EncodeToString(bs.span.SpanId)\n\treply.Traceparent = otlpclient.TraceparentFromProtobufSpan(bs.span, bs.config.GetIsRecording()).Encode()\n\n\tts, err := time.Parse(time.RFC3339Nano, bse.Timestamp)\n\tif err != nil {\n\t\treply.Error = fmt.Sprintf(\"%s\", err)\n\t\treturn err\n\t}\n\n\tevent := otlpclient.NewProtobufSpanEvent()\n\tevent.Name = bse.Name\n\tevent.TimeUnixNano = uint64(ts.UnixNano())\n\tevent.Attributes = otlpclient.StringMapAttrsToProtobuf(bse.Attributes)\n\n\tbs.span.Events = append(bs.span.Events, event)\n\n\treturn nil\n}\n\n// Wait is a no-op RPC for validating the background server is up and running.\nfunc (bs BgSpan) Wait(in, reply *struct{}) error {\n\treturn nil\n}\n\n// End takes a BgEnd (empty) struct, replies with the usual trace info, then\n// ends the span end exits the background process.\nfunc (bs BgSpan) End(in *BgEnd, reply *BgSpan) error {\n\t// handle --attrs arg to span end by retrieving and merging with/overwriting existing attribtues\n\tattrs := make(map[string]string)\n\tfor k, v := range otlpclient.SpanAttributesToStringMap(bs.span) {\n\t\tattrs[k] = v\n\t}\n\tfor key, value := range in.Attributes {\n\t\tattrs[key] = value\n\t}\n\t// handle --status-code and --status-description args to span end\n\tc := bs.config.WithStatusCode(in.StatusCode).WithStatusDescription(in.StatusDesc).WithAttributes(attrs)\n\totlpclient.SetSpanStatus(bs.span, c.StatusCode, c.StatusDescription)\n\tbs.span.Attributes = otlpclient.StringMapAttrsToProtobuf(c.Attributes)\n\n\t// running the shutdown as a goroutine prevents the client from getting an\n\t// error here when the server gets closed. defer didn't do the trick.\n\tgo bs.shutdown()\n\treturn nil\n}\n\n// bgServer is a handle for a span background server.\ntype bgServer struct {\n\tsockfile string\n\tlistener net.Listener\n\tquit     chan struct{}\n\twg       sync.WaitGroup\n\tconfig   Config\n}\n\n// createBgServer opens a new span background server on a unix socket and\n// returns with the server ready to go. Not expected to block.\nfunc createBgServer(ctx context.Context, sockfile string, span *tracepb.Span) *bgServer {\n\tvar err error\n\tconfig := getConfig(ctx)\n\n\tbgs := bgServer{\n\t\tsockfile: sockfile,\n\t\tquit:     make(chan struct{}),\n\t\tconfig:   config,\n\t}\n\n\t// TODO: be safer?\n\tif err = os.RemoveAll(sockfile); err != nil {\n\t\tconfig.SoftFail(\"failed while cleaning up for socket file '%s': %s\", sockfile, err)\n\t}\n\n\tbgspan := BgSpan{\n\t\tTraceID:  hex.EncodeToString(span.TraceId),\n\t\tSpanID:   hex.EncodeToString(span.SpanId),\n\t\tconfig:   config,\n\t\tspan:     span,\n\t\tshutdown: func() { bgs.Shutdown() },\n\t}\n\t// makes methods on BgSpan available over RPC\n\trpc.Register(&bgspan)\n\n\tbgs.listener, err = net.Listen(\"unix\", sockfile)\n\tif err != nil {\n\t\tconfig.SoftFail(\"unable to listen on unix socket '%s': %s\", sockfile, err)\n\t}\n\n\tbgs.wg.Add(1) // cleanup will block until this is done\n\n\treturn &bgs\n}\n\n// Run will block until shutdown, accepting connections and processing them.\nfunc (bgs *bgServer) Run() {\n\t// TODO: add controls to exit loop\n\tfor {\n\t\tconn, err := bgs.listener.Accept()\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase <-bgs.quit: // quitting gracefully\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tbgs.config.SoftFail(\"error while accepting connection: %s\", err)\n\t\t\t}\n\t\t}\n\n\t\tbgs.wg.Add(1)\n\t\tgo func() {\n\t\t\tdefer conn.Close()\n\t\t\tjsonrpc.ServeConn(conn)\n\t\t\tbgs.wg.Done()\n\t\t}()\n\t}\n}\n\n// Shutdown does a controlled shutdown of the background server. Blocks until\n// the server is turned down cleanly and it's safe to exit.\nfunc (bgs *bgServer) Shutdown() {\n\tos.Remove(bgs.sockfile)\n\tclose(bgs.quit)\n\tbgs.listener.Close()\n\tbgs.wg.Wait()\n}\n\n// createBgClient sets up a client connection to the unix socket jsonrpc server\n// and returns the rpc client handle and a shutdown function that should be\n// deferred.\nfunc createBgClient(config Config) (*rpc.Client, func()) {\n\tsockfile := path.Join(config.BackgroundSockdir, spanBgSockfilename)\n\tstarted := time.Now()\n\ttimeout := config.ParseCliTimeout()\n\n\t// wait for the socket file to show up, polling every 25ms until it does or timeout\n\tfor {\n\t\t_, err := os.Stat(sockfile)\n\t\tif os.IsNotExist(err) {\n\t\t\ttime.Sleep(time.Millisecond * 25) // sleep 25ms between checks\n\t\t} else if err != nil {\n\t\t\tconfig.SoftFail(\"failed to stat file '%s': %s\", sockfile, err)\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\n\t\tif timeout > 0 && time.Since(started) > timeout {\n\t\t\tconfig.SoftFail(\"timeout after %s while waiting for span background socket '%s' to appear\", config.Timeout, sockfile)\n\t\t}\n\t}\n\n\tsock := net.UnixAddr{Name: sockfile, Net: \"unix\"}\n\tconn, err := net.DialUnix(sock.Net, nil, &sock)\n\tif err != nil {\n\t\tconfig.SoftFail(\"unable to connect to span background server at '%s': %s\", config.BackgroundSockdir, err)\n\t}\n\n\treturn jsonrpc.NewClient(conn), func() { conn.Close() }\n}\n"
  },
  {
    "path": "otelcli/span_end.go",
    "content": "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// spanEndCmd represents the span event command\nfunc spanEndCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"end\",\n\t\tShort: \"Make a span background to end itself and exit gracefully\",\n\t\tLong: `Gracefully end a background span and have its process exit.\n\nSee: otel-cli span background\n\n\totel-cli span end --sockdir $sockdir \\\n\t\t--attrs \"output.length=$(wc -l < output.txt | sed -e 's/^[[:space:]]*//')\n`,\n\t\tRun: doSpanEnd,\n\t}\n\n\tdefaults := DefaultConfig()\n\n\tcmd.Flags().BoolVar(&config.Verbose, \"verbose\", defaults.Verbose, \"print errors on failure instead of always being silent\")\n\t// TODO\n\t//cmd.Flags().StringVar(&config.Timeout, \"timeout\", defaults.Timeout, \"timeout for otel-cli operations, all timeouts in otel-cli use this value\")\n\tcmd.Flags().StringVar(&config.BackgroundSockdir, \"sockdir\", defaults.BackgroundSockdir, \"a directory where a socket can be placed safely\")\n\tcmd.MarkFlagRequired(\"sockdir\")\n\n\tcmd.Flags().StringVar(&config.SpanEndTime, \"end\", defaults.SpanEndTime, \"an Unix epoch or RFC3339 timestamp for the end of the span\")\n\n\taddSpanStatusParams(&cmd, config)\n\taddAttrParams(&cmd, config)\n\n\treturn &cmd\n}\n\nfunc doSpanEnd(cmd *cobra.Command, args []string) {\n\tconfig := getConfig(cmd.Context())\n\tclient, shutdown := createBgClient(config)\n\n\trpcArgs := BgEnd{\n\t\tAttributes: config.Attributes,\n\t\tStatusCode: config.StatusCode,\n\t\tStatusDesc: config.StatusDescription,\n\t}\n\n\tres := BgSpan{}\n\terr := client.Call(\"BgSpan.End\", rpcArgs, &res)\n\tif err != nil {\n\t\tconfig.SoftFail(\"error while calling background server rpc BgSpan.End: %s\", err)\n\t}\n\tshutdown()\n\n\ttp, _ := traceparent.Parse(res.Traceparent)\n\tif config.TraceparentPrint {\n\t\ttp.Fprint(os.Stdout, config.TraceparentPrintExport)\n\t}\n}\n"
  },
  {
    "path": "otelcli/span_event.go",
    "content": "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)\n\n// spanEventCmd represents the span event command\nfunc spanEventCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"event\",\n\t\tShort: \"create an OpenTelemetry span event and add it to the background span\",\n\t\tLong: `Create an OpenTelemetry span event as specified and send it out.\n\nSee: otel-cli span background\n\n    sd=$(mktemp -d)\n\totel-cli span background --sockdir $sd\n\totel-cli span event \\\n\t    --sockdir $sd \\\n\t\t--name \"did a cool thing\" \\\n\t\t--time $(date +%s.%N) \\\n\t\t--attrs \"os.kernel=$(uname -r)\"\n`,\n\t\tRun: doSpanEvent,\n\t}\n\n\tdefaults := DefaultConfig()\n\n\tcmd.Flags().SortFlags = false\n\n\tcmd.Flags().BoolVar(&config.Verbose, \"verbose\", defaults.Verbose, \"print errors on failure instead of always being silent\")\n\t// TODO\n\t//spanEventCmd.Flags().StringVar(&config.Timeout, \"timeout\", defaults.Timeout, \"timeout for otel-cli operations, all timeouts in otel-cli use this value\")\n\tcmd.Flags().StringVarP(&config.EventName, \"name\", \"e\", defaults.EventName, \"set the name of the event\")\n\tcmd.Flags().StringVarP(&config.EventTime, \"time\", \"t\", defaults.EventTime, \"the precise time of the event in RFC3339Nano or Unix.nano format\")\n\tcmd.Flags().StringVar(&config.BackgroundSockdir, \"sockdir\", \"\", \"a directory where a socket can be placed safely\")\n\tcmd.MarkFlagRequired(\"sockdir\")\n\n\taddAttrParams(&cmd, config)\n\n\treturn &cmd\n}\n\nfunc doSpanEvent(cmd *cobra.Command, args []string) {\n\tconfig := getConfig(cmd.Context())\n\ttimestamp := config.ParsedEventTime()\n\trpcArgs := BgSpanEvent{\n\t\tName:       config.EventName,\n\t\tTimestamp:  timestamp.Format(time.RFC3339Nano),\n\t\tAttributes: config.Attributes,\n\t}\n\n\tres := BgSpan{}\n\tclient, shutdown := createBgClient(config)\n\tdefer shutdown()\n\terr := client.Call(\"BgSpan.AddEvent\", rpcArgs, &res)\n\tif err != nil {\n\t\tconfig.SoftFail(\"error while calling background server rpc BgSpan.AddEvent: %s\", err)\n\t}\n\n\tif config.TraceparentPrint {\n\t\ttp, err := traceparent.Parse(res.Traceparent)\n\t\tif err != nil {\n\t\t\tconfig.SoftFail(\"Could not parse traceparent: %s\", err)\n\t\t}\n\t\ttp.Fprint(os.Stdout, config.TraceparentPrintExport)\n\t}\n}\n"
  },
  {
    "path": "otelcli/status.go",
    "content": "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\"github.com/equinix-labs/otel-cli/otlpclient\"\n\t\"github.com/spf13/cobra\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// StatusOutput captures all the data we want to print out for this subcommand\n// and is also used in ../main_test.go for automated testing.\ntype StatusOutput struct {\n\tConfig      Config               `json:\"config\"`\n\tSpans       []map[string]string  `json:\"spans\"`\n\tSpanData    map[string]string    `json:\"span_data\"`\n\tEnv         map[string]string    `json:\"env\"`\n\tDiagnostics Diagnostics          `json:\"diagnostics\"`\n\tErrors      otlpclient.ErrorList `json:\"errors\"`\n}\n\nfunc statusCmd(config *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"status\",\n\t\tShort: \"send at least one canary and dump status\",\n\t\tLong: `This subcommand is still experimental and the output format is not yet frozen.\n\nBy default just one canary is sent. When --canary-count is set, that number of canaries\nare sent. If --canary-interval is set, status will sleep the specified duration\nbetween canaries, up to --timeout (default 1s).\n\nExample:\n\totel-cli status\n\totel-cli status --canary-count 10 --canary-interval 10 --timeout 10s\n`,\n\t\tRun: doStatus,\n\t}\n\n\tdefaults := DefaultConfig()\n\tcmd.Flags().IntVar(&config.StatusCanaryCount, \"canary-count\", defaults.StatusCanaryCount, \"number of canaries to send\")\n\tcmd.Flags().StringVar(&config.StatusCanaryInterval, \"canary-interval\", defaults.StatusCanaryInterval, \"number of milliseconds to wait between canaries\")\n\n\taddCommonParams(&cmd, config)\n\taddClientParams(&cmd, config)\n\taddSpanParams(&cmd, config)\n\n\treturn &cmd\n}\n\nfunc doStatus(cmd *cobra.Command, args []string) {\n\tvar err error\n\tvar exitCode int\n\tallSpans := []map[string]string{}\n\n\tctx := cmd.Context()\n\tconfig := getConfig(ctx)\n\tctx, cancel := context.WithDeadline(ctx, time.Now().Add(config.GetTimeout()))\n\tdefer cancel()\n\tctx, client := StartClient(ctx, config)\n\n\tenv := make(map[string]string)\n\tfor _, e := range os.Environ() {\n\t\tparts := strings.SplitN(e, \"=\", 2)\n\t\tif len(parts) == 2 {\n\t\t\t// TODO: this is just enough so I can sleep tonight.\n\t\t\t// should be a list at top of file and needs a flag to turn it off\n\t\t\t// TODO: for sure need to mask OTEL_EXPORTER_OTLP_HEADERS\n\t\t\tif strings.Contains(strings.ToLower(parts[0]), \"token\") || parts[0] == \"OTEL_EXPORTER_OTLP_HEADERS\" {\n\t\t\t\tenv[parts[0]] = \"--- redacted ---\"\n\t\t\t} else {\n\t\t\t\tenv[parts[0]] = parts[1]\n\t\t\t}\n\t\t} else {\n\t\t\tconfig.SoftFail(\"BUG in otel-cli: this shouldn't happen\")\n\t\t}\n\t}\n\n\tvar canaryCount int\n\tvar lastSpan *tracepb.Span\n\tdeadline := time.Now().Add(config.GetTimeout())\n\tinterval := config.ParseStatusCanaryInterval()\n\tfor {\n\t\t// should be rare but a caller could request 0 canaries, in which case the\n\t\t// client will be started and stopped, but no canaries sent\n\t\tif config.StatusCanaryCount == 0 {\n\t\t\t// TODO: remove this after SpanData is eliminated\n\t\t\tlastSpan = otlpclient.NewProtobufSpan()\n\t\t\tlastSpan.Name = \"unsent canary\"\n\t\t\tbreak\n\t\t}\n\n\t\tspan := config.NewProtobufSpan()\n\t\tspan.Name = \"otel-cli status\"\n\t\tif canaryCount > 0 {\n\t\t\tspan.Name = fmt.Sprintf(\"otel-cli status canary %d\", canaryCount)\n\t\t}\n\t\tspan.Kind = tracepb.Span_SPAN_KIND_INTERNAL\n\n\t\t// when doing multiple canaries, child each new span to the previous one\n\t\tif lastSpan != nil {\n\t\t\tspan.TraceId = lastSpan.TraceId\n\t\t\tspan.ParentSpanId = lastSpan.SpanId\n\t\t}\n\t\tlastSpan = span\n\t\tallSpans = append(allSpans, otlpclient.SpanToStringMap(span, nil))\n\n\t\t// send it to the server. ignore errors here, they'll happen for sure\n\t\t// and the base errors will be tunneled up through otlpclient.GetErrorList()\n\t\tctx, _ = otlpclient.SendSpan(ctx, client, config, span)\n\t\tcanaryCount++\n\n\t\tif canaryCount == config.StatusCanaryCount {\n\t\t\tbreak\n\t\t} else if time.Now().After(deadline) {\n\t\t\tbreak\n\t\t} else {\n\t\t\ttime.Sleep(interval)\n\t\t}\n\t}\n\n\tctx, err = client.Stop(ctx)\n\tif err != nil {\n\t\tconfig.SoftFail(\"client.Stop() failed: %s\", err)\n\t}\n\n\t// otlpclient saves all errors to a key in context so they can be used\n\t// to validate assumptions here & in tests\n\terrorList := otlpclient.GetErrorList(ctx)\n\n\t// TODO: does it make sense to turn SpanData into a list of spans?\n\toutData := StatusOutput{\n\t\tConfig: config,\n\t\tEnv:    env,\n\t\tSpans:  allSpans,\n\t\t// use only the last span's data here, leftover from when status only\n\t\t// ever sent one canary\n\t\t// legacy, will be removed once test suite is updated\n\t\tSpanData: map[string]string{\n\t\t\t\"trace_id\":   hex.EncodeToString(lastSpan.TraceId),\n\t\t\t\"span_id\":    hex.EncodeToString(lastSpan.SpanId),\n\t\t\t\"is_sampled\": strconv.FormatBool(config.GetIsRecording()),\n\t\t},\n\t\t// Diagnostics is deprecated, being replaced by Errors below and eventually\n\t\t// another stringmap of stuff that was tunneled through context.Context\n\t\tDiagnostics: Diag,\n\t\tErrors:      errorList,\n\t}\n\n\tjs, err := json.MarshalIndent(outData, \"\", \"    \")\n\tconfig.SoftFailIfErr(err)\n\n\tos.Stdout.Write(js)\n\tos.Stdout.WriteString(\"\\n\")\n\n\tos.Exit(exitCode)\n}\n"
  },
  {
    "path": "otelcli/version.go",
    "content": "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 exits.\nfunc versionCmd(_ *Config) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"version\",\n\t\tShort: \"print otel-cli's version, commit, release date to stdout\",\n\t\tRun:   doVersion,\n\t}\n\n\treturn &cmd\n}\n\nfunc doVersion(cmd *cobra.Command, args []string) {\n\tctx := cmd.Context()\n\tconfig := getConfig(ctx)\n\tfmt.Fprintln(os.Stdout, config.Version)\n}\n\n// FormatVersion pretty-prints the global version, commit, and date values into\n// a string to enable the --version flag. Public to be called from main.\nfunc FormatVersion(version, commit, date string) string {\n\tparts := []string{}\n\n\tif version != \"\" {\n\t\tparts = append(parts, version)\n\t}\n\n\tif commit != \"\" {\n\t\tparts = append(parts, commit)\n\t}\n\n\tif date != \"\" {\n\t\tparts = append(parts, date)\n\t}\n\n\tif len(parts) == 0 {\n\t\tparts = append(parts, \"unknown\")\n\t}\n\n\treturn strings.Join(parts, \" \")\n}\n"
  },
  {
    "path": "otelcli/version_test.go",
    "content": "package otelcli\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestFormatVersion(t *testing.T) {\n\temptyVals := FormatVersion(\"\", \"\", \"\")\n\tif diff := cmp.Diff(\"unknown\", emptyVals); diff != \"\" {\n\t\tt.Fatalf(\"FormatVersion() mismatch (-want +got):\\n%s\", diff)\n\t}\n\n\tversionOnly := FormatVersion(\"0.0000\", \"\", \"\")\n\tif diff := cmp.Diff(\"0.0000\", versionOnly); diff != \"\" {\n\t\tt.Fatalf(\"FormatVersion() mismatch (-want +got):\\n%s\", diff)\n\t}\n\n\tloaded := FormatVersion(\"0.0000\", \"e48e468116baa5bd864f4057fc9a0f0774641f1a\", \"Wed Oct 5 12:28:07 2022 -0400\")\n\tif diff := cmp.Diff(\"0.0000 e48e468116baa5bd864f4057fc9a0f0774641f1a Wed Oct 5 12:28:07 2022 -0400\", loaded); diff != \"\" {\n\t\tt.Fatalf(\"FormatVersion() mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n"
  },
  {
    "path": "otlpclient/otlp_client.go",
    "content": "// Package otlpclient implements a simple OTLP client, directly working with\n// protobuf, gRPC, and net/http with minimal abstractions.\npackage otlpclient\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.17.0\"\n\tcommonpb \"go.opentelemetry.io/proto/otlp/common/v1\"\n\tresourcepb \"go.opentelemetry.io/proto/otlp/resource/v1\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// OTLPClient is an interface that allows for StartClient to return either\n// gRPC or HTTP clients.\ntype OTLPClient interface {\n\tStart(context.Context) (context.Context, error)\n\tUploadTraces(context.Context, []*tracepb.ResourceSpans) (context.Context, error)\n\tStop(context.Context) (context.Context, error)\n}\n\n// OTLPConfig interface defines all of the methods required to configure OTLP clients.\ntype OTLPConfig interface {\n\tGetTlsConfig() *tls.Config\n\tGetIsRecording() bool\n\tGetEndpoint() *url.URL\n\tGetInsecure() bool\n\tGetTimeout() time.Duration\n\tGetHeaders() map[string]string\n\tGetVersion() string\n\tGetServiceName() string\n}\n\n// SendSpan connects to the OTLP server, sends the span, and disconnects.\nfunc SendSpan(ctx context.Context, client OTLPClient, config OTLPConfig, span *tracepb.Span) (context.Context, error) {\n\tif !config.GetIsRecording() {\n\t\treturn ctx, nil\n\t}\n\n\tresourceAttrs, err := resourceAttributes(ctx, config.GetServiceName())\n\tif err != nil {\n\t\treturn ctx, err\n\t}\n\n\trsps := []*tracepb.ResourceSpans{\n\t\t{\n\t\t\tResource: &resourcepb.Resource{\n\t\t\t\tAttributes: resourceAttrs,\n\t\t\t},\n\t\t\tScopeSpans: []*tracepb.ScopeSpans{{\n\t\t\t\tScope: &commonpb.InstrumentationScope{\n\t\t\t\t\tName:                   \"github.com/equinix-labs/otel-cli\",\n\t\t\t\t\tVersion:                config.GetVersion(),\n\t\t\t\t\tAttributes:             []*commonpb.KeyValue{},\n\t\t\t\t\tDroppedAttributesCount: 0,\n\t\t\t\t},\n\t\t\t\tSpans:     []*tracepb.Span{span},\n\t\t\t\tSchemaUrl: semconv.SchemaURL,\n\t\t\t}},\n\t\t\tSchemaUrl: semconv.SchemaURL,\n\t\t},\n\t}\n\n\tctx, err = client.UploadTraces(ctx, rsps)\n\tif err != nil {\n\t\treturn SaveError(ctx, time.Now(), err)\n\t}\n\n\treturn ctx, nil\n}\n\n// resourceAttributes calls the OTel SDK to get automatic resource attrs and\n// returns them converted to []*commonpb.KeyValue for use with protobuf.\nfunc resourceAttributes(ctx context.Context, serviceName string) ([]*commonpb.KeyValue, error) {\n\t// set the service name that will show up in tracing UIs\n\tresOpts := []resource.Option{\n\t\tresource.WithAttributes(semconv.ServiceNameKey.String(serviceName)),\n\t\tresource.WithFromEnv(), // maybe switch to manually loading this envvar?\n\t\t// TODO: make these autodetectors configurable\n\t\t//resource.WithHost(),\n\t\t//resource.WithOS(),\n\t\t//resource.WithProcess(),\n\t\t//resource.WithContainer(),\n\t}\n\n\tres, err := resource.New(ctx, resOpts...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create OpenTelemetry service name resource: %s\", err)\n\t}\n\n\tattrs := []*commonpb.KeyValue{}\n\n\tfor _, attr := range res.Attributes() {\n\t\tav := new(commonpb.AnyValue)\n\n\t\t// does not implement slice types... should be fine?\n\t\tswitch attr.Value.Type() {\n\t\tcase attribute.BOOL:\n\t\t\tav.Value = &commonpb.AnyValue_BoolValue{BoolValue: attr.Value.AsBool()}\n\t\tcase attribute.INT64:\n\t\t\tav.Value = &commonpb.AnyValue_IntValue{IntValue: attr.Value.AsInt64()}\n\t\tcase attribute.FLOAT64:\n\t\t\tav.Value = &commonpb.AnyValue_DoubleValue{DoubleValue: attr.Value.AsFloat64()}\n\t\tcase attribute.STRING:\n\t\t\tav.Value = &commonpb.AnyValue_StringValue{StringValue: attr.Value.AsString()}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"BUG: unable to convert resource attribute, please file an issue\")\n\t\t}\n\n\t\tckv := commonpb.KeyValue{\n\t\t\tKey:   string(attr.Key),\n\t\t\tValue: av,\n\t\t}\n\t\tattrs = append(attrs, &ckv)\n\t}\n\n\treturn attrs, nil\n}\n\n// otlpClientCtxKey is a type for storing otlp client information in context.Context safely.\ntype otlpClientCtxKey string\n\n// TimestampedError is a timestamp + error string, to be stored in an ErrorList\ntype TimestampedError struct {\n\tTimestamp time.Time `json:\"timestamp\"`\n\tError     string    `json:\"error\"`\n}\n\n// ErrorList is a list of TimestampedError\ntype ErrorList []TimestampedError\n\n// errorListKey() returns the typed key used to store the error list in context.\nfunc errorListKey() otlpClientCtxKey {\n\treturn otlpClientCtxKey(\"otlp_errors\")\n}\n\n// GetErrorList retrieves the error list from context and returns it. If the list\n// is uninitialized, it initializes it in the returned context.\nfunc GetErrorList(ctx context.Context) ErrorList {\n\tif cv := ctx.Value(errorListKey()); cv != nil {\n\t\tif l, ok := cv.(ErrorList); ok {\n\t\t\treturn l\n\t\t} else {\n\t\t\tpanic(\"BUG: failed to unwrap error list, please report an issue\")\n\t\t}\n\t} else {\n\t\treturn ErrorList{}\n\t}\n}\n\n// SaveError writes the provided error to the ErrorList in ctx, returning an\n// updated ctx.\nfunc SaveError(ctx context.Context, t time.Time, err error) (context.Context, error) {\n\tif err == nil {\n\t\treturn ctx, nil\n\t}\n\n\t//otelcli.Diag.SetError(err) // legacy, will go away when Diag is removed\n\n\tte := TimestampedError{\n\t\tTimestamp: t,\n\t\tError:     err.Error(),\n\t}\n\n\terrorList := GetErrorList(ctx)\n\tnewList := append(errorList, te)\n\tctx = context.WithValue(ctx, errorListKey(), newList)\n\n\treturn ctx, err\n}\n\n// retry calls the provided function and expects it to return (true, wait, err)\n// to keep retrying, and (false, wait, err) to stop retrying and return.\n// The wait value is a time.Duration so the server can recommend a backoff\n// and it will be followed.\n//\n// This is a minimal retry mechanism that backs off linearly, 100ms at a time,\n// up to a maximum of 5 seconds.\n// While there are many robust implementations of retries out there, this one\n// is just ~20 LoC and seems to work fine for otel-cli's modest needs. It should\n// be rare for otel-cli to have a long timeout in the first place, and when it\n// does, maybe it's ok to wait a few seconds.\n// TODO: provide --otlp-retries (or something like that) option on CLI\n// TODO: --otlp-retry-sleep? --otlp-retry-timeout?\n// TODO: span events? hmm... feels weird to plumb spans this deep into the client\n// but it's probably fine?\nfunc retry(ctx context.Context, _ OTLPConfig, fun retryFun) (context.Context, error) {\n\tdeadline, haveDL := ctx.Deadline()\n\tif !haveDL {\n\t\treturn ctx, fmt.Errorf(\"BUG in otel-cli: no deadline set before retry()\")\n\t}\n\tsleep := time.Duration(0)\n\tfor {\n\t\tif ctx, keepGoing, wait, err := fun(ctx); err != nil {\n\t\t\tif keepGoing {\n\t\t\t\tif wait > 0 {\n\t\t\t\t\tif time.Now().Add(wait).After(deadline) {\n\t\t\t\t\t\t// wait will be after deadline, give up now\n\t\t\t\t\t\treturn SaveError(ctx, time.Now(), err)\n\t\t\t\t\t}\n\t\t\t\t\ttime.Sleep(wait)\n\t\t\t\t} else {\n\t\t\t\t\ttime.Sleep(sleep)\n\t\t\t\t}\n\n\t\t\t\tif time.Now().After(deadline) {\n\t\t\t\t\treturn SaveError(ctx, time.Now(), err)\n\t\t\t\t}\n\n\t\t\t\t// linearly increase sleep time up to 5 seconds\n\t\t\t\tif sleep < time.Second*5 {\n\t\t\t\t\tsleep = sleep + time.Millisecond*100\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn SaveError(ctx, time.Now(), err)\n\t\t\t}\n\t\t} else {\n\t\t\treturn ctx, nil\n\t\t}\n\t}\n}\n\n// retryFun is the function signature for functions passed to retry().\n// Return (false, 0, err) to stop retrying. Return (true, 0, err) to continue\n// retrying until timeout. Set the middle wait arg to a time.Duration to\n// sleep a requested amount of time before next try\ntype retryFun func(ctx context.Context) (ctxOut context.Context, keepGoing bool, wait time.Duration, err error)\n"
  },
  {
    "path": "otlpclient/otlp_client_grpc.go",
    "content": "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\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n\t\"google.golang.org/genproto/googleapis/rpc/errdetails\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n)\n\n// GrpcClient holds the state for gRPC connections.\ntype GrpcClient struct {\n\tconn   *grpc.ClientConn\n\tclient coltracepb.TraceServiceClient\n\tconfig OTLPConfig\n}\n\n// NewGrpcClient returns a fresh GrpcClient ready to Start.\nfunc NewGrpcClient(config OTLPConfig) *GrpcClient {\n\tc := GrpcClient{config: config}\n\treturn &c\n}\n\n// Start configures and starts the connection to the gRPC server in the background.\nfunc (gc *GrpcClient) Start(ctx context.Context) (context.Context, error) {\n\tvar err error\n\tendpointURL := gc.config.GetEndpoint()\n\thost := endpointURL.Hostname()\n\tif endpointURL.Port() != \"\" {\n\t\thost = host + \":\" + endpointURL.Port()\n\t}\n\n\tgrpcOpts := []grpc.DialOption{}\n\n\tif gc.config.GetInsecure() {\n\t\tgrpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\t} else {\n\t\tgrpcOpts = append(grpcOpts, grpc.WithTransportCredentials(credentials.NewTLS(gc.config.GetTlsConfig())))\n\t}\n\n\tgc.conn, err = grpc.DialContext(ctx, host, grpcOpts...)\n\tif err != nil {\n\t\treturn ctx, fmt.Errorf(\"could not connect to gRPC/OTLP: %w\", err)\n\t}\n\n\tgc.client = coltracepb.NewTraceServiceClient(gc.conn)\n\n\treturn ctx, nil\n}\n\n// UploadTraces takes a list of protobuf spans and sends them out, doing retries\n// on some errors as needed.\n// TODO: look into grpc.WaitForReady(), esp for status use cases\nfunc (gc *GrpcClient) UploadTraces(ctx context.Context, rsps []*tracepb.ResourceSpans) (context.Context, error) {\n\t// add headers onto the request\n\theaders := gc.config.GetHeaders()\n\tif len(headers) > 0 {\n\t\tmd := metadata.New(headers)\n\t\tctx = metadata.NewOutgoingContext(ctx, md)\n\t}\n\n\treq := coltracepb.ExportTraceServiceRequest{ResourceSpans: rsps}\n\n\treturn retry(ctx, gc.config, func(innerCtx context.Context) (context.Context, bool, time.Duration, error) {\n\t\tetsr, err := gc.client.Export(innerCtx, &req)\n\t\treturn processGrpcStatus(innerCtx, etsr, err)\n\t})\n}\n\n// Stop closes the connection to the gRPC server.\nfunc (gc *GrpcClient) Stop(ctx context.Context) (context.Context, error) {\n\treturn ctx, gc.conn.Close()\n}\n\nfunc processGrpcStatus(ctx context.Context, _ *coltracepb.ExportTraceServiceResponse, err error) (context.Context, bool, time.Duration, error) {\n\tif err == nil {\n\t\t// success!\n\t\treturn ctx, false, 0, nil\n\t}\n\n\tst := status.Convert(err)\n\tif st.Code() == codes.OK {\n\t\t// apparently this can happen and is a success\n\t\treturn ctx, false, 0, nil\n\t}\n\n\tvar ri *errdetails.RetryInfo\n\tfor _, d := range st.Details() {\n\t\tif t, ok := d.(*errdetails.RetryInfo); ok {\n\t\t\tri = t\n\t\t}\n\t}\n\n\t// handle retriable codes, somewhat lifted from otel collector\n\tswitch st.Code() {\n\tcase codes.Aborted,\n\t\tcodes.Canceled,\n\t\tcodes.DataLoss,\n\t\tcodes.DeadlineExceeded,\n\t\tcodes.OutOfRange,\n\t\tcodes.Unavailable:\n\t\treturn ctx, true, 0, err\n\tcase codes.ResourceExhausted:\n\t\t// only retry this one if RetryInfo was set\n\t\tif ri != nil && ri.RetryDelay != nil {\n\t\t\t// when RetryDelay is available, pass it back to the retry loop\n\t\t\t// so it can sleep that duration\n\t\t\twait := time.Duration(ri.RetryDelay.Seconds)*time.Second + time.Duration(ri.RetryDelay.Nanos)*time.Nanosecond\n\t\t\treturn ctx, true, wait, err\n\t\t} else {\n\t\t\treturn ctx, false, 0, err\n\t\t}\n\tdefault:\n\t\t// don't retry anything else\n\t\treturn ctx, false, 0, err\n\t}\n\n}\n"
  },
  {
    "path": "otlpclient/otlp_client_grpc_test.go",
    "content": "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.opentelemetry.io/proto/otlp/collector/trace/v1\"\n\t\"google.golang.org/genproto/googleapis/rpc/errdetails\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/durationpb\"\n)\n\nfunc TestProcessGrpcStatus(t *testing.T) {\n\tfor i, tc := range []struct {\n\t\tetsr      *coltracepb.ExportTraceServiceResponse\n\t\tkeepgoing bool\n\t\terr       error\n\t\twait      time.Duration\n\t}{\n\t\t// simple success\n\t\t{\n\t\t\tetsr:      &coltracepb.ExportTraceServiceResponse{},\n\t\t\tkeepgoing: false,\n\t\t\terr:       nil,\n\t\t},\n\t\t// partial success, no retry\n\t\t{\n\t\t\tetsr: &coltracepb.ExportTraceServiceResponse{\n\t\t\t\tPartialSuccess: &coltracepb.ExportTracePartialSuccess{\n\t\t\t\t\tRejectedSpans: 2,\n\t\t\t\t\tErrorMessage:  \"whoops\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeepgoing: false,\n\t\t\terr:       status.Errorf(codes.OK, \"\"),\n\t\t},\n\t\t// failure, unretriable\n\t\t{\n\t\t\tetsr:      &coltracepb.ExportTraceServiceResponse{},\n\t\t\tkeepgoing: false,\n\t\t\terr:       status.Errorf(codes.PermissionDenied, \"test: permission denied\"),\n\t\t},\n\t\t// failure, retry\n\t\t{\n\t\t\tetsr:      &coltracepb.ExportTraceServiceResponse{},\n\t\t\tkeepgoing: true,\n\t\t\terr:       status.Errorf(codes.DeadlineExceeded, \"test: should retry\"),\n\t\t},\n\t\t// failure, retry, with server-provided wait\n\t\t{\n\t\t\tetsr:      &coltracepb.ExportTraceServiceResponse{},\n\t\t\tkeepgoing: true,\n\t\t\terr:       retryWithInfo(1),\n\t\t\twait:      time.Second,\n\t\t},\n\t} {\n\t\tctx := context.Background()\n\t\t_, kg, wait, err := processGrpcStatus(ctx, tc.etsr, tc.err)\n\n\t\tif kg != tc.keepgoing {\n\t\t\tt.Errorf(\"keepgoing value returned %t but expected %t in test %d\", kg, tc.keepgoing, i)\n\t\t}\n\n\t\tif tc.err == nil && err != nil {\n\t\t\tt.Errorf(\"received an unexpected error on test %d\", i)\n\t\t} else if tc.err != nil && err == nil {\n\t\t\tt.Errorf(\"did not receive expected error on test %d\", i)\n\t\t} else if tc.err == nil && err == nil {\n\t\t\t// success, do nothing\n\t\t} else if diff := cmp.Diff(tc.err.Error(), err.Error()); diff != \"\" {\n\t\t\tt.Errorf(\"error did not match testcase for test %d: %s\", i, diff)\n\t\t}\n\n\t\tif wait != tc.wait {\n\t\t\tt.Errorf(\"expected a wait value of %d but got %d\", tc.wait, wait)\n\t\t}\n\t}\n}\n\nfunc retryWithInfo(wait int64) error {\n\tvar err error\n\tst := status.New(codes.ResourceExhausted, \"Server unavailable\")\n\tif wait > 0 {\n\t\tst, err = st.WithDetails(&errdetails.RetryInfo{\n\t\t\tRetryDelay: &durationpb.Duration{Seconds: wait},\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(\"error creating retry info\")\n\t\t}\n\t}\n\n\treturn st.Err()\n}\n"
  },
  {
    "path": "otlpclient/otlp_client_http.go",
    "content": "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\tcoltracepb \"go.opentelemetry.io/proto/otlp/collector/trace/v1\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n\t\"google.golang.org/genproto/googleapis/rpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// HttpClient holds state information for HTTP/OTLP.\ntype HttpClient struct {\n\tclient *http.Client\n\tconfig OTLPConfig\n}\n\n// NewHttpClient returns an initialized HttpClient.\nfunc NewHttpClient(config OTLPConfig) *HttpClient {\n\tc := HttpClient{config: config}\n\treturn &c\n}\n\n// Start sets up the client configuration.\n// TODO: see if there's a way to background start http2 connections?\nfunc (hc *HttpClient) Start(ctx context.Context) (context.Context, error) {\n\tif hc.config.GetInsecure() {\n\t\thc.client = &http.Client{Timeout: hc.config.GetTimeout()}\n\t} else {\n\t\thc.client = &http.Client{\n\t\t\tTimeout: hc.config.GetTimeout(),\n\t\t\tTransport: &http.Transport{\n\t\t\t\tDialTLS: func(network, addr string) (net.Conn, error) {\n\t\t\t\t\treturn tls.Dial(network, addr, hc.config.GetTlsConfig())\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\treturn ctx, nil\n}\n\n// UploadTraces sends the protobuf spans up to the HTTP server.\nfunc (hc *HttpClient) UploadTraces(ctx context.Context, rsps []*tracepb.ResourceSpans) (context.Context, error) {\n\tmsg := coltracepb.ExportTraceServiceRequest{ResourceSpans: rsps}\n\tprotoMsg, err := proto.Marshal(&msg)\n\tif err != nil {\n\t\treturn ctx, fmt.Errorf(\"failed to marshal trace service request: %w\", err)\n\t}\n\tbody := bytes.NewBuffer(protoMsg)\n\n\tendpointURL := hc.config.GetEndpoint()\n\treq, err := http.NewRequest(\"POST\", endpointURL.String(), body)\n\tif err != nil {\n\t\treturn ctx, fmt.Errorf(\"failed to create HTTP POST request: %w\", err)\n\t}\n\n\tfor k, v := range hc.config.GetHeaders() {\n\t\treq.Header.Add(k, v)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-protobuf\")\n\n\treturn retry(ctx, hc.config, func(context.Context) (context.Context, bool, time.Duration, error) {\n\t\tvar body []byte\n\t\tresp, err := hc.client.Do(req)\n\t\tif uerr, ok := err.(*url.Error); ok {\n\t\t\t// e.g. http on https, un-retriable error, quit now\n\t\t\treturn ctx, false, 0, uerr\n\t\t} else {\n\t\t\tbody, err = io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn ctx, true, 0, fmt.Errorf(\"io.Readall of response body failed: %w\", err)\n\t\t\t}\n\t\t\tresp.Body.Close()\n\n\t\t\treturn processHTTPStatus(ctx, resp, body)\n\t\t}\n\t})\n}\n\n// processHTTPStatus takes the http.Response and body, returning the same bool, error\n// as retryFunc. Mostly it's broken out so it can be unit tested.\nfunc processHTTPStatus(ctx context.Context, resp *http.Response, body []byte) (context.Context, bool, time.Duration, error) {\n\t// #262 a vendor OTLP server is out of spec and returns JSON instead of protobuf\n\tctype := resp.Header.Get(\"Content-Type\")\n\tif ctype == \"\" {\n\t\treturn ctx, false, 0, fmt.Errorf(\"server is out of specification: Content-Type header is missing or mangled\")\n\t} else if ctype != \"application/x-protobuf\" {\n\t\treturn ctx, false, 0, fmt.Errorf(\"server is out of specification: expected content type application/x-protobuf but got %q\", ctype)\n\t}\n\n\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\t// success & partial success\n\t\t// spec says server MUST send 200 OK, we'll be generous and accept any 200\n\t\tetsr := coltracepb.ExportTraceServiceResponse{}\n\t\terr := proto.Unmarshal(body, &etsr)\n\t\tif err != nil {\n\t\t\t// if the server's sending garbage, no point in retrying\n\t\t\treturn ctx, false, 0, fmt.Errorf(\"unmarshal of server response failed: %w\", err)\n\t\t}\n\n\t\tif partial := etsr.GetPartialSuccess(); partial != nil && partial.RejectedSpans > 0 {\n\t\t\t// spec says to stop retrying and drop rejected spans\n\t\t\treturn ctx, false, 0, fmt.Errorf(\"partial success. %d spans were rejected\", partial.GetRejectedSpans())\n\n\t\t} else {\n\t\t\t// full success!\n\t\t\treturn ctx, false, 0, nil\n\t\t}\n\t} else if resp.StatusCode == 429 || resp.StatusCode == 502 || resp.StatusCode == 503 || resp.StatusCode == 504 {\n\t\t// 429, 502, 503, and 504 must be retried according to spec\n\t\treturn ctx, true, 0, fmt.Errorf(\"server responded with retriable code %d\", resp.StatusCode)\n\t} else if resp.StatusCode >= 300 && resp.StatusCode < 400 {\n\t\t// spec doesn't say anything about 300's, ignore body and assume they're errors and unretriable\n\t\treturn ctx, false, 0, fmt.Errorf(\"server returned unsupported code %d\", resp.StatusCode)\n\t} else if resp.StatusCode >= 400 {\n\t\t// https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#failures-1\n\t\tst := status.Status{}\n\t\terr := proto.Unmarshal(body, &st)\n\t\tif err != nil {\n\t\t\treturn ctx, false, 0, fmt.Errorf(\"unmarshal of server status failed: %w\", err)\n\t\t} else {\n\t\t\treturn ctx, false, 0, fmt.Errorf(\"server returned unretriable code %d with status: %s\", resp.StatusCode, st.GetMessage())\n\t\t}\n\t}\n\n\t// should never happen\n\treturn ctx, false, 0, fmt.Errorf(\"BUG: fell through error checking with status code %d\", resp.StatusCode)\n}\n\n// Stop does nothing for HTTP, for now. It exists to fulfill the interface.\nfunc (hc *HttpClient) Stop(ctx context.Context) (context.Context, error) {\n\treturn ctx, nil\n}\n"
  },
  {
    "path": "otlpclient/otlp_client_http_test.go",
    "content": "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.opentelemetry.io/proto/otlp/collector/trace/v1\"\n\t\"google.golang.org/genproto/googleapis/rpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/anypb\"\n)\n\nfunc TestProcessHTTPStatus(t *testing.T) {\n\theaders := http.Header{\n\t\t\"Content-Type\": []string{\"application/x-protobuf\"},\n\t}\n\n\tfor _, tc := range []struct {\n\t\tresp      *http.Response\n\t\tbody      []byte\n\t\tkeepgoing bool\n\t\terr       error\n\t}{\n\t\t// simple success\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 200,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      etsrSuccessBody(),\n\t\t\tkeepgoing: false,\n\t\t\terr:       nil,\n\t\t},\n\t\t// partial success\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 200,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      etsrPartialSuccessBody(),\n\t\t\tkeepgoing: false,\n\t\t\terr:       fmt.Errorf(\"partial success. 1 spans were rejected\"),\n\t\t},\n\t\t// failure, unretriable\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 500,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      errorBody(500, \"xyz\"),\n\t\t\tkeepgoing: false,\n\t\t\terr:       fmt.Errorf(\"server returned unretriable code 500 with status: xyz\"),\n\t\t},\n\t\t// failures the spec requires retries for, 429, 502, 503, 504\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 429,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      errorBody(429, \"xyz\"),\n\t\t\tkeepgoing: true,\n\t\t\terr:       fmt.Errorf(\"server responded with retriable code 429\"),\n\t\t},\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 502,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      errorBody(502, \"xyz\"),\n\t\t\tkeepgoing: true,\n\t\t\terr:       fmt.Errorf(\"server responded with retriable code 502\"),\n\t\t},\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 503,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      errorBody(503, \"xyz\"),\n\t\t\tkeepgoing: true,\n\t\t\terr:       fmt.Errorf(\"server responded with retriable code 503\"),\n\t\t},\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 504,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      errorBody(504, \"xyz\"),\n\t\t\tkeepgoing: true,\n\t\t\terr:       fmt.Errorf(\"server responded with retriable code 504\"),\n\t\t},\n\t\t// 300's are unsupported\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 301,\n\t\t\t\tHeader:     headers,\n\t\t\t},\n\t\t\tbody:      errorBody(301, \"xyz\"),\n\t\t\tkeepgoing: false,\n\t\t\terr:       fmt.Errorf(\"server returned unsupported code 301\"),\n\t\t},\n\t\t// shouldn't happen in the real world...\n\t\t{\n\t\t\tresp:      &http.Response{Header: headers},\n\t\t\tbody:      []byte(\"\"),\n\t\t\tkeepgoing: false,\n\t\t\terr:       fmt.Errorf(\"BUG: fell through error checking with status code 0\"),\n\t\t},\n\t\t// return a decent error for out-of-spec servers that return JSON after a protobuf payload\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 200,\n\t\t\t\tHeader:     http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t\t},\n\t\t\tbody:      []byte(`{\"some\": \"json\"}`),\n\t\t\tkeepgoing: false,\n\t\t\terr:       fmt.Errorf(`server is out of specification: expected content type application/x-protobuf but got \"application/json\"`),\n\t\t},\n\t\t// spec requires headers so report that as a server problem too\n\t\t{\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: 200,\n\t\t\t\t// no headers!\n\t\t\t},\n\t\t\tbody:      []byte(\"\"),\n\t\t\tkeepgoing: false,\n\t\t\terr:       fmt.Errorf(\"server is out of specification: Content-Type header is missing or mangled\"),\n\t\t},\n\t} {\n\t\tctx := context.Background()\n\t\t_, kg, _, err := processHTTPStatus(ctx, tc.resp, tc.body)\n\n\t\tif kg != tc.keepgoing {\n\t\t\tt.Errorf(\"keepgoing value returned %t but expected %t\", kg, tc.keepgoing)\n\t\t}\n\n\t\tif tc.err == nil && err != nil {\n\t\t\tt.Errorf(\"received an unexpected error\")\n\t\t} else if tc.err != nil && err == nil {\n\t\t\tt.Errorf(\"did not receive expected error\")\n\t\t} else if tc.err == nil && err == nil {\n\t\t\tcontinue // pass\n\t\t} else if diff := cmp.Diff(tc.err.Error(), err.Error()); diff != \"\" {\n\t\t\tt.Errorf(\"error did not match testcase: %s\", diff)\n\t\t}\n\t}\n}\n\nfunc etsrSuccessBody() []byte {\n\tetsr := coltracepb.ExportTraceServiceResponse{\n\t\tPartialSuccess: nil,\n\t}\n\tb, _ := proto.Marshal(&etsr)\n\treturn b\n}\n\nfunc etsrPartialSuccessBody() []byte {\n\tetsr := coltracepb.ExportTraceServiceResponse{\n\t\tPartialSuccess: &coltracepb.ExportTracePartialSuccess{\n\t\t\tRejectedSpans: 1,\n\t\t\tErrorMessage:  \"xyz\",\n\t\t},\n\t}\n\tb, _ := proto.Marshal(&etsr)\n\treturn b\n}\n\nfunc errorBody(c int32, message string) []byte {\n\tst := status.Status{\n\t\tCode:    c,\n\t\tMessage: message,\n\t\tDetails: []*anypb.Any{},\n\t}\n\tb, _ := proto.Marshal(&st)\n\treturn b\n}\n"
  },
  {
    "path": "otlpclient/otlp_client_null.go",
    "content": "package otlpclient\n\nimport (\n\t\"context\"\n\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// NullClient is an OTLP client backend for non-recording mode that drops\n// all data and never returns errors.\ntype NullClient struct{}\n\n// NewNullClient returns a fresh NullClient ready to Start.\nfunc NewNullClient(config OTLPConfig) *NullClient {\n\treturn &NullClient{}\n}\n\n// Start fulfills the interface and does nothing.\nfunc (nc *NullClient) Start(ctx context.Context) (context.Context, error) {\n\treturn ctx, nil\n}\n\n// UploadTraces fulfills the interface and does nothing.\nfunc (nc *NullClient) UploadTraces(ctx context.Context, rsps []*tracepb.ResourceSpans) (context.Context, error) {\n\treturn ctx, nil\n}\n\n// Stop fulfills the interface and does nothing.\nfunc (gc *NullClient) Stop(ctx context.Context) (context.Context, error) {\n\treturn ctx, nil\n}\n"
  },
  {
    "path": "otlpclient/otlp_client_test.go",
    "content": "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 TestErrorLists(t *testing.T) {\n\tnow := time.Now()\n\n\tfor _, tc := range []struct {\n\t\tcall func(context.Context) context.Context\n\t\twant ErrorList\n\t}{\n\t\t{\n\t\t\tcall: func(ctx context.Context) context.Context {\n\t\t\t\terr := fmt.Errorf(\"\")\n\t\t\t\tctx, _ = SaveError(ctx, now, err)\n\t\t\t\treturn ctx\n\t\t\t},\n\t\t\twant: ErrorList{\n\t\t\t\tTimestampedError{now, \"\"},\n\t\t\t},\n\t\t},\n\t} {\n\t\tctx := context.Background()\n\t\tctx = tc.call(ctx)\n\t\tlist := GetErrorList(ctx)\n\n\t\tif len(list) < len(tc.want) {\n\t\t\tt.Errorf(\"got %d errors but expected %d\", len(tc.want), len(list))\n\t\t}\n\n\t\t// TODO: sort?\n\t\tif diff := cmp.Diff(list, tc.want); diff != \"\" {\n\t\t\tt.Errorf(\"error list mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "otlpclient/protobuf_span.go",
    "content": "package otlpclient\n\n// Implements just enough sugar on the OTel Protocol Buffers span definition\n// to support otel-cli and no more.\n//\n// otel-cli does a few things that are awkward via the opentelemetry-go APIs\n// which are restricted for good reasons.\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/equinix-labs/otel-cli/w3c/traceparent\"\n\tcommonpb \"go.opentelemetry.io/proto/otlp/common/v1\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\ntype SpanConfig interface {\n}\n\n// NewProtobufSpan returns an initialized OpenTelemetry protobuf Span.\nfunc NewProtobufSpan() *tracepb.Span {\n\tnow := time.Now()\n\tspan := tracepb.Span{\n\t\tTraceId:                GetEmptyTraceId(),\n\t\tSpanId:                 GetEmptySpanId(),\n\t\tTraceState:             \"\",\n\t\tParentSpanId:           []byte{},\n\t\tName:                   \"BUG IN OTEL-CLI: unset\",\n\t\tKind:                   tracepb.Span_SPAN_KIND_CLIENT,\n\t\tStartTimeUnixNano:      uint64(now.UnixNano()),\n\t\tEndTimeUnixNano:        uint64(now.UnixNano()),\n\t\tAttributes:             []*commonpb.KeyValue{},\n\t\tDroppedAttributesCount: 0,\n\t\tEvents:                 []*tracepb.Span_Event{},\n\t\tDroppedEventsCount:     0,\n\t\tLinks:                  []*tracepb.Span_Link{},\n\t\tDroppedLinksCount:      0,\n\t\tStatus: &tracepb.Status{\n\t\t\tCode:    tracepb.Status_STATUS_CODE_UNSET,\n\t\t\tMessage: \"\",\n\t\t},\n\t}\n\n\treturn &span\n}\n\n// NewProtobufSpanEvent creates a new span event protobuf struct with reasonable\n// defaults and returns it.\nfunc NewProtobufSpanEvent() *tracepb.Span_Event {\n\tnow := time.Now()\n\treturn &tracepb.Span_Event{\n\t\tTimeUnixNano: uint64(now.UnixNano()),\n\t\tAttributes:   []*commonpb.KeyValue{},\n\t}\n}\n\n// SetSpanStatus checks for status code error in the config and sets the\n// span's 2 values as appropriate.\n// Only set status description when an error status.\n// https://github.com/open-telemetry/opentelemetry-specification/blob/480a19d702470563d32a870932be5ddae798079c/specification/trace/api.md#set-status\nfunc SetSpanStatus(span *tracepb.Span, status string, message string) {\n\tstatusCode := SpanStatusStringToInt(status)\n\tif statusCode != tracepb.Status_STATUS_CODE_UNSET {\n\t\tspan.Status.Code = statusCode\n\t\tspan.Status.Message = message\n\t}\n}\n\n// GetEmptyTraceId returns a 16-byte trace id that's all zeroes.\nfunc GetEmptyTraceId() []byte {\n\treturn []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}\n}\n\n// GetEmptySpanId returns an 8-byte span id that's all zeroes.\nfunc GetEmptySpanId() []byte {\n\treturn []byte{0, 0, 0, 0, 0, 0, 0, 0}\n}\n\n// GenerateTraceId generates a random 16 byte trace id\nfunc GenerateTraceId() []byte {\n\tbuf := make([]byte, 16)\n\t_, err := rand.Read(buf)\n\tif err != nil {\n\t\t// should never happen, crash when it does\n\t\tpanic(\"failed to generate random data for trace id: \" + err.Error())\n\t}\n\treturn buf\n}\n\n// GenerateSpanId generates a random 8 byte span id\nfunc GenerateSpanId() []byte {\n\tbuf := make([]byte, 8)\n\t_, err := rand.Read(buf)\n\tif err != nil {\n\t\t// should never happen, crash when it does\n\t\tpanic(\"failed to generate random data for span id: \" + err.Error())\n\t}\n\treturn buf\n}\n\n// SpanKindIntToString takes an integer/constant protobuf span kind value\n// and returns the string representation used in otel-cli.\nfunc SpanKindIntToString(kind tracepb.Span_SpanKind) string {\n\tswitch kind {\n\tcase tracepb.Span_SPAN_KIND_CLIENT:\n\t\treturn \"client\"\n\tcase tracepb.Span_SPAN_KIND_SERVER:\n\t\treturn \"server\"\n\tcase tracepb.Span_SPAN_KIND_PRODUCER:\n\t\treturn \"producer\"\n\tcase tracepb.Span_SPAN_KIND_CONSUMER:\n\t\treturn \"consumer\"\n\tcase tracepb.Span_SPAN_KIND_INTERNAL:\n\t\treturn \"internal\"\n\tdefault:\n\t\treturn \"unspecified\"\n\t}\n}\n\n// SpanKindIntToString takes a string representation of a span kind and\n// returns the OTel protobuf integer/constant.\nfunc SpanKindStringToInt(kind string) tracepb.Span_SpanKind {\n\tswitch kind {\n\tcase \"client\":\n\t\treturn tracepb.Span_SPAN_KIND_CLIENT\n\tcase \"server\":\n\t\treturn tracepb.Span_SPAN_KIND_SERVER\n\tcase \"producer\":\n\t\treturn tracepb.Span_SPAN_KIND_PRODUCER\n\tcase \"consumer\":\n\t\treturn tracepb.Span_SPAN_KIND_CONSUMER\n\tcase \"internal\":\n\t\treturn tracepb.Span_SPAN_KIND_INTERNAL\n\tdefault:\n\t\treturn tracepb.Span_SPAN_KIND_UNSPECIFIED\n\t}\n}\n\n// SpanStatusStringToInt takes a supported string span status and returns the otel\n// constant for it. Returns default of Unset on no match.\nfunc SpanStatusStringToInt(status string) tracepb.Status_StatusCode {\n\tswitch status {\n\tcase \"unset\":\n\t\treturn tracepb.Status_STATUS_CODE_UNSET\n\tcase \"ok\":\n\t\treturn tracepb.Status_STATUS_CODE_OK\n\tcase \"error\":\n\t\treturn tracepb.Status_STATUS_CODE_ERROR\n\tdefault:\n\t\treturn tracepb.Status_STATUS_CODE_UNSET\n\t}\n}\n\n// StringMapAttrsToProtobuf takes a map of string:string, such as that from --attrs\n// and returns them in an []*commonpb.KeyValue\nfunc StringMapAttrsToProtobuf(attributes map[string]string) []*commonpb.KeyValue {\n\tout := []*commonpb.KeyValue{}\n\n\tfor k, v := range attributes {\n\t\tav := new(commonpb.AnyValue)\n\n\t\t// try to parse as numbers, and fall through to string\n\t\tif i, err := strconv.ParseInt(v, 0, 64); err == nil {\n\t\t\tav.Value = &commonpb.AnyValue_IntValue{IntValue: i}\n\t\t} else if f, err := strconv.ParseFloat(v, 64); err == nil {\n\t\t\tav.Value = &commonpb.AnyValue_DoubleValue{DoubleValue: f}\n\t\t} else if b, err := strconv.ParseBool(v); err == nil {\n\t\t\tav.Value = &commonpb.AnyValue_BoolValue{BoolValue: b}\n\t\t} else {\n\t\t\tav.Value = &commonpb.AnyValue_StringValue{StringValue: v}\n\t\t}\n\n\t\takv := commonpb.KeyValue{\n\t\t\tKey:   k,\n\t\t\tValue: av,\n\t\t}\n\n\t\tout = append(out, &akv)\n\t}\n\n\treturn out\n}\n\n// SpanAttributesToStringMap converts the span's attributes to a string map.\nfunc SpanAttributesToStringMap(span *tracepb.Span) map[string]string {\n\tout := make(map[string]string)\n\tfor _, attr := range span.Attributes {\n\t\tout[attr.Key] = AnyValueToString(attr.GetValue())\n\t}\n\treturn out\n}\n\n// ResourceAttributesToStringMap converts the ResourceSpan's resource attributes to a string map.\n// Only used by tests for now.\nfunc ResourceAttributesToStringMap(rss *tracepb.ResourceSpans) map[string]string {\n\tif rss == nil {\n\t\treturn map[string]string{}\n\t}\n\n\tout := make(map[string]string)\n\tfor _, attr := range rss.Resource.Attributes {\n\t\tout[attr.Key] = AnyValueToString(attr.GetValue())\n\t}\n\treturn out\n}\n\n// AnyValueToString coverts a commonpb.KeyValue attribute to a string.\nfunc AnyValueToString(v *commonpb.AnyValue) string {\n\tif _, ok := v.Value.(*commonpb.AnyValue_StringValue); ok {\n\t\treturn v.GetStringValue()\n\t} else if _, ok := v.Value.(*commonpb.AnyValue_IntValue); ok {\n\t\treturn strconv.FormatInt(v.GetIntValue(), 10)\n\t} else if _, ok := v.Value.(*commonpb.AnyValue_DoubleValue); ok {\n\t\treturn strconv.FormatFloat(v.GetDoubleValue(), byte('f'), -1, 64)\n\t} else if _, ok := v.Value.(*commonpb.AnyValue_ArrayValue); ok {\n\t\tvalues := v.GetArrayValue().GetValues()\n\t\tstrValues := make([]string, len(values))\n\t\tfor i, v := range values {\n\t\t\t// recursively convert to string\n\t\t\tstrValues[i] = AnyValueToString(v)\n\t\t}\n\t\treturn strings.Join(strValues, \",\")\n\t}\n\n\treturn \"\"\n}\n\n// SpanToStringMap converts a span with some extra data into a stringmap.\n// Only used by tests for now.\nfunc SpanToStringMap(span *tracepb.Span, rss *tracepb.ResourceSpans) map[string]string {\n\tif span == nil {\n\t\treturn map[string]string{}\n\t}\n\treturn map[string]string{\n\t\t\"trace_id\":           hex.EncodeToString(span.GetTraceId()),\n\t\t\"span_id\":            hex.EncodeToString(span.GetSpanId()),\n\t\t\"parent_span_id\":     hex.EncodeToString(span.GetParentSpanId()),\n\t\t\"name\":               span.Name,\n\t\t\"kind\":               SpanKindIntToString(span.GetKind()),\n\t\t\"start\":              strconv.FormatUint(span.StartTimeUnixNano, 10),\n\t\t\"end\":                strconv.FormatUint(span.EndTimeUnixNano, 10),\n\t\t\"attributes\":         flattenStringMap(SpanAttributesToStringMap(span), \"{}\"),\n\t\t\"service_attributes\": flattenStringMap(ResourceAttributesToStringMap(rss), \"{}\"),\n\t\t\"status_code\":        strconv.FormatInt(int64(span.Status.GetCode()), 10),\n\t\t\"status_description\": span.Status.GetMessage(),\n\t}\n}\n\n// TraceparentFromProtobufSpan builds a Traceparent struct from the provided span.\nfunc TraceparentFromProtobufSpan(span *tracepb.Span, recording bool) traceparent.Traceparent {\n\treturn traceparent.Traceparent{\n\t\tVersion:     0,\n\t\tTraceId:     span.TraceId,\n\t\tSpanId:      span.SpanId,\n\t\tSampling:    recording,\n\t\tInitialized: true,\n\t}\n}\n\n// flattenStringMap takes a string map and returns it flattened into a string with\n// keys sorted lexically so it should be mostly consistent enough for comparisons\n// and printing. Output is k=v,k=v style like attributes input.\nfunc flattenStringMap(mp map[string]string, emptyValue string) string {\n\tif len(mp) == 0 {\n\t\treturn emptyValue\n\t}\n\n\tvar out string\n\tkeys := make([]string, len(mp)) // for sorting\n\tvar i int\n\tfor k := range mp {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\tsort.Strings(keys)\n\n\tfor i, k := range keys {\n\t\tout = out + k + \"=\" + mp[k]\n\t\tif i == len(keys)-1 {\n\t\t\tbreak\n\t\t}\n\t\tout = out + \",\"\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "otlpclient/protobuf_span_test.go",
    "content": "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 TestNewProtobufSpan(t *testing.T) {\n\tspan := NewProtobufSpan()\n\n\t// no tmuch to test since it's just an initialized struct\n\tif len(span.Name) < 1 {\n\t\tt.Error(\"span name should default to non-empty string\")\n\t}\n\n\tif span.ParentSpanId == nil {\n\t\tt.Error(\"span parent must not be nil\")\n\t}\n\n\tif span.Attributes == nil {\n\t\tt.Error(\"span attributes must not be nil\")\n\t}\n\n\tif span.Events == nil {\n\t\tt.Error(\"span events must not be nil\")\n\t}\n\n\tif span.Links == nil {\n\t\tt.Error(\"span links must not be nil\")\n\t}\n}\n\nfunc TestNewProtobufSpanEvent(t *testing.T) {\n\tevt := NewProtobufSpanEvent()\n\n\t// similar to span above, just run the code and make sure\n\t// it doesn't blow up\n\tif evt.Attributes == nil {\n\t\tt.Error(\"span event attributes must not be nil\")\n\t}\n}\n\nfunc TestGenerateTraceId(t *testing.T) {\n\ttid := GenerateTraceId()\n\n\tif bytes.Equal(tid, GetEmptyTraceId()) {\n\t\tt.Error(\"generated trace id is all zeroes and should be any other random value\")\n\t}\n\n\tif len(tid) != 16 {\n\t\tt.Error(\"generated trace id must be 16 bytes\")\n\t}\n}\n\nfunc TestGenerateSpanId(t *testing.T) {\n\tsid := GenerateSpanId()\n\n\tif bytes.Equal(sid, GetEmptySpanId()) {\n\t\tt.Error(\"generated span id is all zeroes and should be any other random value\")\n\t}\n\n\tif len(sid) != 8 {\n\t\tt.Error(\"generated span id must be 8 bytes\")\n\t}\n}\n\nfunc TestSpanKindStringToInt(t *testing.T) {\n\tfor _, testcase := range []struct {\n\t\tname string\n\t\twant tracepb.Span_SpanKind\n\t}{\n\t\t{\n\t\t\tname: \"client\",\n\t\t\twant: tracepb.Span_SPAN_KIND_CLIENT,\n\t\t},\n\t\t{\n\t\t\tname: \"server\",\n\t\t\twant: tracepb.Span_SPAN_KIND_SERVER,\n\t\t},\n\t\t{\n\t\t\tname: \"producer\",\n\t\t\twant: tracepb.Span_SPAN_KIND_PRODUCER,\n\t\t},\n\t\t{\n\t\t\tname: \"consumer\",\n\t\t\twant: tracepb.Span_SPAN_KIND_CONSUMER,\n\t\t},\n\t\t{\n\t\t\tname: \"internal\",\n\t\t\twant: tracepb.Span_SPAN_KIND_INTERNAL,\n\t\t},\n\t\t{\n\t\t\tname: \"unspecified\",\n\t\t\twant: tracepb.Span_SPAN_KIND_UNSPECIFIED,\n\t\t},\n\t\t{\n\t\t\tname: \"speledwrong\",\n\t\t\twant: tracepb.Span_SPAN_KIND_UNSPECIFIED,\n\t\t},\n\t} {\n\t\tt.Run(testcase.name, func(t *testing.T) {\n\t\t\tout := SpanKindStringToInt(testcase.name)\n\t\t\tif out != testcase.want {\n\t\t\t\tt.Errorf(\"returned the wrong value, '%q', for '%s'\", out, testcase.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSpanKindIntToString(t *testing.T) {\n\tfor _, testcase := range []struct {\n\t\twant string\n\t\thave tracepb.Span_SpanKind\n\t}{\n\t\t{\n\t\t\thave: tracepb.Span_SPAN_KIND_CLIENT,\n\t\t\twant: \"client\",\n\t\t},\n\t\t{\n\t\t\thave: tracepb.Span_SPAN_KIND_SERVER,\n\t\t\twant: \"server\",\n\t\t},\n\t\t{\n\t\t\thave: tracepb.Span_SPAN_KIND_PRODUCER,\n\t\t\twant: \"producer\",\n\t\t},\n\t\t{\n\t\t\thave: tracepb.Span_SPAN_KIND_CONSUMER,\n\t\t\twant: \"consumer\",\n\t\t},\n\t\t{\n\t\t\thave: tracepb.Span_SPAN_KIND_INTERNAL,\n\t\t\twant: \"internal\",\n\t\t},\n\t\t{\n\t\t\thave: tracepb.Span_SPAN_KIND_UNSPECIFIED,\n\t\t\twant: \"unspecified\",\n\t\t},\n\t} {\n\t\tname := strconv.Itoa(int(testcase.have)) + \" => \" + testcase.want\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tout := SpanKindIntToString(testcase.have)\n\t\t\tif out != testcase.want {\n\t\t\t\tt.Errorf(\"returned the wrong value, '%q', for %d\", out, int(testcase.have))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSpanStatusStringToInt(t *testing.T) {\n\n\tfor _, testcase := range []struct {\n\t\tname string\n\t\twant tracepb.Status_StatusCode\n\t}{\n\t\t{\n\t\t\tname: \"unset\",\n\t\t\twant: tracepb.Status_STATUS_CODE_UNSET,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\twant: tracepb.Status_STATUS_CODE_OK,\n\t\t},\n\t\t{\n\t\t\tname: \"error\",\n\t\t\twant: tracepb.Status_STATUS_CODE_ERROR,\n\t\t},\n\t\t{\n\t\t\tname: \"cromulent\",\n\t\t\twant: tracepb.Status_STATUS_CODE_UNSET,\n\t\t},\n\t} {\n\t\tt.Run(testcase.name, func(t *testing.T) {\n\t\t\tout := SpanStatusStringToInt(testcase.name)\n\t\t\tif out != testcase.want {\n\t\t\t\tt.Errorf(\"otelSpanStatus returned the wrong value, '%q', for '%s'\", out, testcase.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCliAttrsToOtel(t *testing.T) {\n\n\ttestAttrs := map[string]string{\n\t\t\"test 1 - string\":      \"isn't testing fun?\",\n\t\t\"test 2 - int64\":       \"111111111\",\n\t\t\"test 3 - float\":       \"2.4391111\",\n\t\t\"test 4 - bool, true\":  \"true\",\n\t\t\"test 5 - bool, false\": \"false\",\n\t\t\"test 6 - bool, True\":  \"True\",\n\t\t\"test 7 - bool, False\": \"False\",\n\t}\n\n\totelAttrs := StringMapAttrsToProtobuf(testAttrs)\n\n\t// can't count on any ordering from map -> array\n\tfor _, attr := range otelAttrs {\n\t\tkey := string(attr.Key)\n\t\tswitch key {\n\t\tcase \"test 1 - string\":\n\t\t\tif attr.Value.GetStringValue() != testAttrs[key] {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got '%s'\", testAttrs[key], key, attr.Value.GetStringValue())\n\t\t\t}\n\t\tcase \"test 2 - int64\":\n\t\t\tif attr.Value.GetIntValue() != 111111111 {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got %d\", testAttrs[key], key, attr.Value.GetIntValue())\n\t\t\t}\n\t\tcase \"test 3 - float\":\n\t\t\tif attr.Value.GetDoubleValue() != 2.4391111 {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got %f\", testAttrs[key], key, attr.Value.GetDoubleValue())\n\t\t\t}\n\t\tcase \"test 4 - bool, true\":\n\t\t\tif attr.Value.GetBoolValue() != true {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got %t\", testAttrs[key], key, attr.Value.GetBoolValue())\n\t\t\t}\n\t\tcase \"test 5 - bool, false\":\n\t\t\tif attr.Value.GetBoolValue() != false {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got %t\", testAttrs[key], key, attr.Value.GetBoolValue())\n\t\t\t}\n\t\tcase \"test 6 - bool, True\":\n\t\t\tif attr.Value.GetBoolValue() != true {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got %t\", testAttrs[key], key, attr.Value.GetBoolValue())\n\t\t\t}\n\t\tcase \"test 7 - bool, False\":\n\t\t\tif attr.Value.GetBoolValue() != false {\n\t\t\t\tt.Errorf(\"expected value '%s' for key '%s' but got %t\", testAttrs[key], key, attr.Value.GetBoolValue())\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "otlpserver/grpcserver.go",
    "content": "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/proto/otlp/collector/trace/v1\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/metadata\"\n)\n\n// GrpcServer is a gRPC/OTLP server handle.\ntype GrpcServer struct {\n\tserver   *grpc.Server\n\tcallback Callback\n\tstoponce sync.Once\n\tstopper  chan struct{}\n\tstopdone chan struct{}\n\tdoneonce sync.Once\n\tcoltracepb.UnimplementedTraceServiceServer\n}\n\n// NewGrpcServer takes a callback and stop function and returns a Server ready\n// to run with .Serve().\nfunc NewGrpcServer(cb Callback, stop Stopper) *GrpcServer {\n\ts := GrpcServer{\n\t\tserver:   grpc.NewServer(),\n\t\tcallback: cb,\n\t\tstopper:  make(chan struct{}),\n\t\tstopdone: make(chan struct{}, 1),\n\t}\n\n\tcoltracepb.RegisterTraceServiceServer(s.server, &s)\n\n\t// single place to stop the server, used by timeout and max-spans\n\tgo func() {\n\t\t<-s.stopper\n\t\tstop(&s)\n\t\ts.server.GracefulStop()\n\t}()\n\n\treturn &s\n}\n\n// ServeGRPC takes a listener and starts the GRPC server on that listener.\n// Blocks until Stop() is called.\nfunc (gs *GrpcServer) Serve(listener net.Listener) error {\n\terr := gs.server.Serve(listener)\n\tgs.stopdone <- struct{}{}\n\treturn err\n}\n\n// ListenAndServeGRPC starts a TCP listener then starts the GRPC server using\n// ServeGRPC for you.\nfunc (gs *GrpcServer) ListenAndServe(otlpEndpoint string) {\n\tlistener, err := net.Listen(\"tcp\", otlpEndpoint)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to listen on OTLP endpoint %q: %s\", otlpEndpoint, err)\n\t}\n\tif err := gs.Serve(listener); err != nil {\n\t\tlog.Fatalf(\"failed to serve: %s\", err)\n\t}\n}\n\n// Stop sends a value to the server shutdown goroutine so it stops GRPC\n// and calls the stop function given to newServer. Safe to call multiple times.\nfunc (gs *GrpcServer) Stop() {\n\tgs.stoponce.Do(func() {\n\t\tgs.stopper <- struct{}{}\n\t})\n}\n\n// StopWait stops the server and waits for it to affirm shutdown.\nfunc (gs *GrpcServer) StopWait() {\n\tgs.Stop()\n\tgs.doneonce.Do(func() {\n\t\t<-gs.stopdone\n\t})\n}\n\n// Export implements the gRPC server interface for exporting messages.\nfunc (gs *GrpcServer) Export(ctx context.Context, req *coltracepb.ExportTraceServiceRequest) (*coltracepb.ExportTraceServiceResponse, error) {\n\t// OTLP/gRPC headers are passed in metadata, copy them to serverMeta\n\t// for now. This isn't ideal but gets them exposed to the test suite.\n\theaders := make(map[string]string)\n\tif md, ok := metadata.FromIncomingContext(ctx); ok {\n\t\tfor mdk := range md {\n\t\t\tvals := md.Get(mdk)\n\t\t\tbuf := bytes.NewBuffer([]byte{})\n\t\t\tcsv.NewWriter(buf).WriteAll([][]string{vals})\n\t\t\theaders[mdk] = buf.String()\n\t\t}\n\t}\n\n\tdone := doCallback(ctx, gs.callback, req, headers, map[string]string{\"proto\": \"grpc\"})\n\tif done {\n\t\tgo gs.StopWait()\n\t}\n\treturn &coltracepb.ExportTraceServiceResponse{}, nil\n}\n"
  },
  {
    "path": "otlpserver/httpserver.go",
    "content": "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.io/proto/otlp/collector/trace/v1\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// HttpServer is a handle for otlp over http/protobuf.\ntype HttpServer struct {\n\tserver   *http.Server\n\tcallback Callback\n}\n\n// NewServer takes a callback and stop function and returns a Server ready\n// to run with .Serve().\nfunc NewHttpServer(cb Callback, stop Stopper) *HttpServer {\n\ts := HttpServer{\n\t\tserver:   &http.Server{},\n\t\tcallback: cb,\n\t}\n\n\ts.server.Handler = &s\n\n\treturn &s\n}\n\n// ServeHTTP processes every request as if it is a trace regardless of\n// method and path or anything else.\nfunc (hs *HttpServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {\n\tdata, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error while reading request body: %s\", err)\n\t}\n\n\tmsg := coltracepb.ExportTraceServiceRequest{}\n\tswitch req.Header.Get(\"Content-Type\") {\n\tcase \"application/x-protobuf\":\n\t\tproto.Unmarshal(data, &msg)\n\tcase \"application/json\":\n\t\tjson.Unmarshal(data, &msg)\n\tdefault:\n\t\trw.WriteHeader(http.StatusNotAcceptable)\n\t}\n\n\tmeta := map[string]string{\n\t\t\"method\":       req.Method,\n\t\t\"proto\":        req.Proto,\n\t\t\"content-type\": req.Header.Get(\"Content-Type\"),\n\t\t\"host\":         req.Host,\n\t\t\"uri\":          req.RequestURI,\n\t}\n\n\theaders := make(map[string]string)\n\tfor k := range req.Header {\n\t\theaders[k] = req.Header.Get(k)\n\t}\n\n\tdone := doCallback(req.Context(), hs.callback, &msg, headers, meta)\n\tif done {\n\t\tgo hs.StopWait()\n\t}\n}\n\n// ServeHttp takes a listener and starts the HTTP server on that listener.\n// Blocks until Stop() is called.\nfunc (hs *HttpServer) Serve(listener net.Listener) error {\n\terr := hs.server.Serve(listener)\n\treturn err\n}\n\n// ListenAndServeHttp starts a TCP listener then starts the HTTP server using\n// ServeHttp for you.\nfunc (hs *HttpServer) ListenAndServe(otlpEndpoint string) {\n\tlistener, err := net.Listen(\"tcp\", otlpEndpoint)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to listen on OTLP endpoint %q: %s\", otlpEndpoint, err)\n\t}\n\tif err := hs.Serve(listener); err != nil {\n\t\tlog.Fatalf(\"failed to serve: %s\", err)\n\t}\n}\n\n// Stop closes the http server and all active connections immediately.\nfunc (hs *HttpServer) Stop() {\n\ths.server.Close()\n}\n\n// StopWait stops the http server gracefully.\nfunc (hs *HttpServer) StopWait() {\n\ths.server.Shutdown(context.Background())\n}\n"
  },
  {
    "path": "otlpserver/server.go",
    "content": "// otlpserver is an OTLP server with HTTP and gRPC backends available.\n// It takes a lot of shortcuts to keep things simple and is not intended\n// to be used as a serious OTLP service. Primarily it is for the test\n// suite and also supports the otel-cli server features.\npackage otlpserver\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\tcolv1 \"go.opentelemetry.io/proto/otlp/collector/trace/v1\"\n\ttracepb \"go.opentelemetry.io/proto/otlp/trace/v1\"\n)\n\n// Callback is a type for the function passed to newServer that is\n// called for each incoming span.\ntype Callback func(context.Context, *tracepb.Span, []*tracepb.Span_Event, *tracepb.ResourceSpans, map[string]string, map[string]string) bool\n\n// Stopper is the function passed to newServer to be called when the\n// server is shut down.\ntype Stopper func(OtlpServer)\n\n// OtlpServer abstracts the minimum interface required for an OTLP\n// server to be either HTTP or gRPC (but not both, for now).\ntype OtlpServer interface {\n\tListenAndServe(otlpEndpoint string)\n\tServe(listener net.Listener) error\n\tStop()\n\tStopWait()\n}\n\n// NewServer will start the requested server protocol, one of grpc, http/protobuf,\n// and http/json.\nfunc NewServer(protocol string, cb Callback, stop Stopper) OtlpServer {\n\tswitch protocol {\n\tcase \"grpc\":\n\t\treturn NewGrpcServer(cb, stop)\n\tcase \"http\":\n\t\treturn NewHttpServer(cb, stop)\n\t}\n\n\treturn nil\n}\n\n// doCallback unwraps the OTLP service request and calls the callback\n// for each span in the request.\nfunc doCallback(ctx context.Context, cb Callback, req *colv1.ExportTraceServiceRequest, headers map[string]string, serverMeta map[string]string) bool {\n\trss := req.GetResourceSpans()\n\tfor _, resource := range rss {\n\t\tscopeSpans := resource.GetScopeSpans()\n\t\tfor _, ss := range scopeSpans {\n\t\t\tfor _, span := range ss.GetSpans() {\n\t\t\t\tevents := span.GetEvents()\n\t\t\t\tif events == nil {\n\t\t\t\t\tevents = []*tracepb.Span_Event{}\n\t\t\t\t}\n\n\t\t\t\tdone := cb(ctx, span, events, resource, headers, serverMeta)\n\t\t\t\tif done {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "release/Dockerfile",
    "content": "# While the top-level Dockerfile is set up for local development on otel-cli,\n# this Dockerfile is only for release.\n#\n# We use the Alpine base image to get the TLS trust store and not much else.\n# The ca-certificates-bundle packet is pre-installed in the base so no\n# additional packages are required.\nFROM alpine:latest\nENTRYPOINT [\"/otel-cli\"]\nCOPY otel-cli /\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:base\"\n  ]\n}\n"
  },
  {
    "path": "tls_for_test.go",
    "content": "package main_test\n\n/*\n * This file implements a certificate authority and certs for testing otel-cli's\n * TLS settings.\n *\n * Do NOT copy this code for production systems. It makes a few compromises to\n * optimize for testing and ephemeral certs that are totally inappropriate for\n * use in settings where security matters.\n */\n\nimport (\n\t\"bytes\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype TlsSettings struct {\n\tcaFile            string\n\tcaPrivKeyFile     string\n\tserverFile        string\n\tserverPrivKeyFile string\n\tclientFile        string\n\tclientPrivKeyFile string\n\tserverTLSConf     *tls.Config\n\tclientTLSConf     *tls.Config\n\tcertpool          *x509.CertPool\n}\n\nfunc generateTLSData(t *testing.T) TlsSettings {\n\tvar err error\n\tvar out TlsSettings\n\n\texpire := time.Now().Add(time.Hour)\n\n\t// ------------- CA -------------\n\n\tca := &x509.Certificate{\n\t\tSerialNumber:          big.NewInt(4317),\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              expire,\n\t\tIsCA:                  true,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// create a private key\n\tcaPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating ca private key: %s\", err)\n\t}\n\n\t// create a cert on the CA with the ^^ private key\n\tcaBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating ca cert: %s\", err)\n\t}\n\n\t// get the PEM encoding that the tests will use\n\tcaPEM := new(bytes.Buffer)\n\tpem.Encode(caPEM, &pem.Block{Type: \"CERTIFICATE\", Bytes: caBytes})\n\tout.caFile = pemToTempFile(t, \"ca-cert\", caPEM)\n\n\tcaPrivKeyPEM := new(bytes.Buffer)\n\tcaPrivKeyBytes, err := x509.MarshalECPrivateKey(caPrivKey)\n\tif err != nil {\n\t\tt.Fatalf(\"error marshaling server cert: %s\", err)\n\t}\n\tpem.Encode(caPrivKeyPEM, &pem.Block{Type: \"EC PRIVATE KEY\", Bytes: caPrivKeyBytes})\n\tout.caPrivKeyFile = pemToTempFile(t, \"ca-privkey\", caPrivKeyPEM)\n\n\tout.certpool = x509.NewCertPool()\n\tout.certpool.AppendCertsFromPEM(caPEM.Bytes())\n\n\tdata := new(bytes.Buffer)\n\tpem.Encode(data, &pem.Block{Type: \"EC PRIVATE KEY\", Bytes: caPrivKeyBytes})\n\n\t// ------------- server -------------\n\n\tserverCert := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(4318),\n\t\tIPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     expire,\n\t}\n\n\tserverPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating server private key: %s\", err)\n\t}\n\n\tserverBytes, err := x509.CreateCertificate(rand.Reader, serverCert, ca, &serverPrivKey.PublicKey, caPrivKey)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating server cert: %s\", err)\n\t}\n\n\tserverPEM := new(bytes.Buffer)\n\tpem.Encode(serverPEM, &pem.Block{Type: \"CERTIFICATE\", Bytes: serverBytes})\n\tout.serverFile = pemToTempFile(t, \"server-cert\", serverPEM)\n\n\tserverPrivKeyPEM := new(bytes.Buffer)\n\tserverPrivKeyBytes, err := x509.MarshalECPrivateKey(serverPrivKey)\n\tif err != nil {\n\t\tt.Fatalf(\"error marshaling server cert: %s\", err)\n\t}\n\tpem.Encode(serverPrivKeyPEM, &pem.Block{Type: \"EC PRIVATE KEY\", Bytes: serverPrivKeyBytes})\n\tout.serverPrivKeyFile = pemToTempFile(t, \"server-privkey\", serverPrivKeyPEM)\n\n\tserverCertPair, err := tls.X509KeyPair(serverPEM.Bytes(), serverPrivKeyPEM.Bytes())\n\tif err != nil {\n\t\tt.Fatalf(\"error generating server cert pair: %s\", err)\n\t}\n\n\tout.serverTLSConf = &tls.Config{\n\t\tClientCAs:    out.certpool,\n\t\tCertificates: []tls.Certificate{serverCertPair},\n\t}\n\n\t// ------------- client -------------\n\n\tclientCert := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(4319),\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     expire,\n\t}\n\n\tclientPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating client private key: %s\", err)\n\t}\n\n\tclientBytes, err := x509.CreateCertificate(rand.Reader, clientCert, ca, &clientPrivKey.PublicKey, caPrivKey)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating client cert: %s\", err)\n\t}\n\n\tclientPEM := new(bytes.Buffer)\n\tpem.Encode(clientPEM, &pem.Block{Type: \"CERTIFICATE\", Bytes: clientBytes})\n\tout.clientFile = pemToTempFile(t, \"client-cert\", clientPEM)\n\n\tclientPrivKeyPEM := new(bytes.Buffer)\n\tclientPrivKeyBytes, err := x509.MarshalECPrivateKey(clientPrivKey)\n\tif err != nil {\n\t\tt.Fatalf(\"error marshaling client cert: %s\", err)\n\t}\n\tpem.Encode(clientPrivKeyPEM, &pem.Block{Type: \"EC PRIVATE KEY\", Bytes: clientPrivKeyBytes})\n\tout.clientPrivKeyFile = pemToTempFile(t, \"client-privkey\", clientPrivKeyPEM)\n\n\tout.clientTLSConf = &tls.Config{\n\t\tServerName: \"localhost\",\n\t}\n\n\treturn out\n}\n\nfunc (t TlsSettings) cleanup() {\n\tos.Remove(t.caFile)\n\tos.Remove(t.caPrivKeyFile)\n\tos.Remove(t.clientFile)\n\tos.Remove(t.clientPrivKeyFile)\n\tos.Remove(t.serverFile)\n\tos.Remove(t.serverPrivKeyFile)\n}\n\nfunc pemToTempFile(t *testing.T, tmpl string, buf *bytes.Buffer) string {\n\ttmp, err := os.CreateTemp(os.TempDir(), \"otel-cli-test-\"+tmpl+\"-pem\")\n\tif err != nil {\n\t\tt.Fatalf(\"error creating temp file: %s\", err)\n\t}\n\ttmp.Write(buf.Bytes())\n\ttmp.Close()\n\treturn tmp.Name()\n}\n"
  },
  {
    "path": "w3c/traceparent/traceparent.go",
    "content": "// Package traceparent contains a lightweight implementation of W3C\n// traceparent parsing, loading from files and environment, and the reverse.\npackage traceparent\n\nimport (\n\t\"bufio\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar traceparentRe *regexp.Regexp\nvar emptyTraceId = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}\nvar emptySpanId = []byte{0, 0, 0, 0, 0, 0, 0, 0}\n\nfunc init() {\n\t// only anchored at the front because traceparents can include more things\n\t// per the standard but only the first 4 are required for our uses\n\ttraceparentRe = regexp.MustCompile(\"^([[:xdigit:]]{2})-([[:xdigit:]]{32})-([[:xdigit:]]{16})-([[:xdigit:]]{2})\")\n}\n\n// Traceparent represents a parsed W3C traceparent.\ntype Traceparent struct {\n\tVersion     int\n\tTraceId     []byte\n\tSpanId      []byte\n\tSampling    bool\n\tInitialized bool\n}\n\n// Encode returns the traceparent as a W3C formatted string.\nfunc (tp Traceparent) Encode() string {\n\tvar sampling int\n\tvar traceId, spanId string\n\tif tp.Sampling {\n\t\tsampling = 1\n\t}\n\n\tif len(tp.TraceId) == 0 {\n\t\ttraceId = hex.EncodeToString(emptyTraceId)\n\t} else {\n\t\ttraceId = tp.TraceIdString()\n\t}\n\n\tif len(tp.SpanId) == 0 {\n\t\tspanId = hex.EncodeToString(emptySpanId)\n\t} else {\n\t\tspanId = tp.SpanIdString()\n\t}\n\n\treturn fmt.Sprintf(\"%02d-%s-%s-%02d\", tp.Version, traceId, spanId, sampling)\n}\n\n// TraceIdString returns the trace id in string form.\nfunc (tp Traceparent) TraceIdString() string {\n\tif len(tp.TraceId) == 0 {\n\t\treturn hex.EncodeToString(emptyTraceId)\n\t} else {\n\t\treturn hex.EncodeToString(tp.TraceId)\n\t}\n}\n\n// SpanIdString returns the span id in string form.\nfunc (tp Traceparent) SpanIdString() string {\n\tif len(tp.SpanId) == 0 {\n\t\treturn hex.EncodeToString(emptySpanId)\n\t} else {\n\t\treturn hex.EncodeToString(tp.SpanId)\n\t}\n}\n\n// LoadFromFile reads a traceparent from filename and returns a\n// context with the traceparent set. The format for the file as written is\n// just a bare traceparent string. Whitespace, \"export \" and \"TRACEPARENT=\" are\n// stripped automatically so the file can also be a valid shell snippet.\nfunc LoadFromFile(filename string) (Traceparent, error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\terrOut := fmt.Errorf(\"could not open file '%s' for read: %s\", filename, err)\n\t\t// only fatal when the tp carrier file is required explicitly, otherwise\n\t\t// just silently return the unmodified context\n\t\treturn Traceparent{}, errOut\n\t}\n\tdefer file.Close()\n\n\t// only use the line that contains TRACEPARENT\n\tvar tp string\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\t// printSpanData emits comments with trace id and span id, ignore those\n\t\tif strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t} else if strings.Contains(strings.ToUpper(line), \"TRACEPARENT\") {\n\t\t\ttp = line\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// silently fail if no traceparent was found\n\tif tp == \"\" {\n\t\treturn Traceparent{}, nil\n\t}\n\n\t// clean 'export TRACEPARENT=' and 'TRACEPARENT=' off the output\n\ttp = strings.TrimPrefix(tp, \"export \")\n\ttp = strings.TrimPrefix(tp, \"TRACEPARENT=\")\n\n\tif !traceparentRe.MatchString(tp) {\n\t\treturn Traceparent{}, fmt.Errorf(\"file '%s' was read but does not contain a valid traceparent\", filename)\n\t}\n\n\treturn Parse(tp)\n}\n\n// SaveToFile takes a context and filename and writes the tp from\n// that context into the specified file.\nfunc (tp Traceparent) SaveToFile(carrierFile string, export bool) error {\n\tfile, err := os.OpenFile(carrierFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failure opening file '%s' for write: %w\", carrierFile, err)\n\t}\n\tdefer file.Close()\n\n\treturn tp.Fprint(file, export)\n}\n\n// Fprint formats a traceparent into otel-cli's shell-compatible text format.\n// If the second/export param is true, the statement will be prepended with \"export \"\n// so it can be easily sourced in a shell script.\nfunc (tp Traceparent) Fprint(target io.Writer, export bool) error {\n\t// --tp-export will print \"export TRACEPARENT\" so it's\n\t// one less step to print to a file & source, or eval\n\tvar exported string\n\tif export {\n\t\texported = \"export \"\n\t}\n\n\ttraceId := tp.TraceIdString()\n\tspanId := tp.SpanIdString()\n\t_, err := fmt.Fprintf(target, \"# trace id: %s\\n#  span id: %s\\n%sTRACEPARENT=%s\\n\", traceId, spanId, exported, tp.Encode())\n\treturn err\n}\n\n// LoadFromEnv loads the traceparent from the environment variable\n// TRACEPARENT and sets it in the returned Go context.\nfunc LoadFromEnv() (Traceparent, error) {\n\ttp := os.Getenv(\"TRACEPARENT\")\n\tif tp == \"\" {\n\t\treturn Traceparent{}, nil\n\t}\n\n\treturn Parse(tp)\n}\n\n// Parse parses a string traceparent and returns the struct.\nfunc Parse(tp string) (Traceparent, error) {\n\tvar err error\n\tout := Traceparent{}\n\n\tparts := traceparentRe.FindStringSubmatch(tp)\n\tif len(parts) != 5 {\n\t\treturn out, fmt.Errorf(\"could not parse invalid traceparent %q\", tp)\n\t}\n\n\tout.Version, err = strconv.Atoi(parts[1])\n\tif err != nil {\n\t\treturn out, fmt.Errorf(\"could not parse traceparent version component in %q\", tp)\n\t}\n\n\tout.TraceId, err = hex.DecodeString(parts[2])\n\tif err != nil {\n\t\treturn out, fmt.Errorf(\"could not parse traceparent trace id component in %q\", tp)\n\t}\n\n\tout.SpanId, err = hex.DecodeString(parts[3])\n\tif err != nil {\n\t\treturn out, fmt.Errorf(\"could not parse traceparent span id component in %q\", tp)\n\t}\n\n\tsampleFlag, err := strconv.ParseInt(parts[4], 10, 64)\n\tif err != nil {\n\t\treturn out, fmt.Errorf(\"could not parse traceparent sampling bits component in %q\", tp)\n\t}\n\tout.Sampling = (sampleFlag == 1)\n\n\t// mark that this is a successfully parsed struct\n\tout.Initialized = true\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "w3c/traceparent/traceparent_test.go",
    "content": "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 *testing.T) {\n\tfor _, tc := range []struct {\n\t\ttp     Traceparent\n\t\texport bool\n\t\twant   string\n\t}{\n\t\t// unconfigured, all zeroes\n\t\t{\n\t\t\ttp: Traceparent{\n\t\t\t\tVersion:     0,\n\t\t\t\tTraceId:     []byte{},\n\t\t\t\tSpanId:      []byte{},\n\t\t\t\tSampling:    false,\n\t\t\t\tInitialized: false,\n\t\t\t},\n\t\t\texport: false,\n\t\t\twant: \"# trace id: 00000000000000000000000000000000\\n\" +\n\t\t\t\t\"#  span id: 0000000000000000\\n\" +\n\t\t\t\t\"TRACEPARENT=00-00000000000000000000000000000000-0000000000000000-00\\n\",\n\t\t},\n\t\t// fully loaded, print all the things\n\t\t{\n\t\t\ttp: Traceparent{\n\t\t\t\tVersion:     0,\n\t\t\t\tTraceId:     []byte{0xfe, 0xdc, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21},\n\t\t\t\tSpanId:      []byte{0xde, 0xea, 0xd6, 0xbb, 0xaa, 0xbb, 0xcc, 0xdd},\n\t\t\t\tSampling:    true,\n\t\t\t\tInitialized: true,\n\t\t\t},\n\t\t\texport: true,\n\t\t\twant: \"# trace id: fedccba987654321fedccba987654321\\n\" +\n\t\t\t\t\"#  span id: deead6bbaabbccdd\\n\" +\n\t\t\t\t\"export TRACEPARENT=00-fedccba987654321fedccba987654321-deead6bbaabbccdd-01\\n\",\n\t\t},\n\t\t// have a traceparent, but sampling is off, the tp should propagate as-is\n\t\t{\n\t\t\ttp: Traceparent{\n\t\t\t\tVersion:     0,\n\t\t\t\tTraceId:     []byte{0xfe, 0xdc, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21},\n\t\t\t\tSpanId:      []byte{0xde, 0xea, 0xd6, 0xbb, 0xaa, 0xbb, 0xcc, 0xdd},\n\t\t\t\tSampling:    false,\n\t\t\t\tInitialized: true,\n\t\t\t},\n\t\t\texport: false,\n\t\t\twant: \"# trace id: fedccba987654321fedccba987654321\\n\" +\n\t\t\t\t\"#  span id: deead6bbaabbccdd\\n\" +\n\t\t\t\t// the traceparent provided should get printed\n\t\t\t\t\"TRACEPARENT=00-fedccba987654321fedccba987654321-deead6bbaabbccdd-00\\n\",\n\t\t},\n\t} {\n\t\tbuf := bytes.NewBuffer([]byte{})\n\t\terr := tc.tp.Fprint(buf, tc.export)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"got an unexpected error: %s\", err)\n\t\t}\n\n\t\tif diff := cmp.Diff(tc.want, buf.String()); diff != \"\" {\n\t\t\tt.Errorf(\"printed tp didn't match expected: (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestLoadTraceparent(t *testing.T) {\n\t// make sure the environment variable isn't polluting test state\n\tos.Unsetenv(\"TRACEPARENT\")\n\n\t// trace id should not change, because there's no envvar and no file\n\ttp, err := LoadFromFile(os.DevNull)\n\tif err != nil {\n\t\tt.Error(\"LoadFromFile returned an unexpected error: %w\", err)\n\t}\n\tif tp.Initialized {\n\t\tt.Error(\"traceparent detected where there should be none\")\n\t}\n\n\t// load from file only\n\ttestFileTp := \"00-f61fc53f926e07a9c3893b1a722e1b65-7a2d6a804f3de137-01\"\n\tfile, err := os.CreateTemp(t.TempDir(), \"go-test-otel-cli\")\n\tif err != nil {\n\t\tt.Fatalf(\"unable to create tempfile for testing: %s\", err)\n\t}\n\tdefer os.Remove(file.Name())\n\t// write in the full shell snippet format so that stripping gets tested\n\t// in this pass too\n\tfile.WriteString(\"export TRACEPARENT=\" + testFileTp)\n\tfile.Close()\n\n\t// actually do the test...\n\ttp, err = LoadFromFile(file.Name())\n\tif err != nil {\n\t\tt.Error(\"LoadFromFile returned an unexpected error: %w\", err)\n\t}\n\tif tp.Encode() != testFileTp {\n\t\tt.Errorf(\"LoadFromFile failed, expected '%s', got '%s'\", testFileTp, tp.Encode())\n\t}\n\n\t// load from environment\n\ttestEnvTp := \"00-b122b620341449410b9cd900c96d459d-aa21cda35388b694-01\"\n\tos.Setenv(\"TRACEPARENT\", testEnvTp)\n\ttp, err = LoadFromEnv()\n\tif err != nil {\n\t\tt.Error(\"LoadFromEnv() returned an unexpected error: %w\", err)\n\t}\n\tif tp.Encode() != testEnvTp {\n\t\tt.Errorf(\"LoadFromEnv() with envvar failed, expected '%s', got '%s'\", testEnvTp, tp.Encode())\n\t}\n}\n\nfunc TestWriteTraceparentToFile(t *testing.T) {\n\ttestTp := \"00-ce1c6ae29edafc52eb6dd223da7d20b4-1c617f036253531c-01\"\n\ttp, err := Parse(testTp)\n\tif err != nil {\n\t\tt.Errorf(\"failed while parsing test TP %q: %s\", testTp, err)\n\t}\n\n\t// create a tempfile for messing with\n\tfile, err := os.CreateTemp(t.TempDir(), \"go-test-otel-cli\")\n\tif err != nil {\n\t\tt.Fatalf(\"unable to create tempfile for testing: %s\", err)\n\t}\n\tfile.Close()\n\tdefer os.Remove(file.Name()) // not strictly necessary\n\n\terr = tp.SaveToFile(file.Name(), false)\n\tif err != nil {\n\t\tt.Error(\"SaveToFile returned an unexpected error: %w\", err)\n\t}\n\n\t// read the data back, it should just be the traceparent string\n\tdata, err := os.ReadFile(file.Name())\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read tempfile '%s': %s\", file.Name(), err)\n\t}\n\tif len(data) == 0 {\n\t\tt.Errorf(\"saveTraceparentToFile wrote %d bytes to the tempfile, expected %d\", len(data), len(testTp))\n\t}\n\n\t// otel is non-recording in tests so the comments in the output will be zeroed\n\t// while the traceparent should come through just fine at the end of file\n\tif !strings.HasSuffix(strings.TrimSpace(string(data)), testTp) {\n\t\tt.Errorf(\"invalid data in traceparent file, expected '%s', got '%s'\", testTp, data)\n\t}\n}\n"
  }
]