Skip to content

Commit

Permalink
remove viper, fix tests, fix and expand envvars (#120)
Browse files Browse the repository at this point in the history
It seems like some documented environment variables never worked, or haven't in a while.
When I wrote the first pass, I included Viper because it was recommended with Cobra, and
maybe a config file will be more useful than I'm imagining right now. A couple things were
broken such that config loading and environment variables via Viper were not working at
all. I think the critical ones like `OTEL_EXPORTER_OTLP_ENDPOINT` worked because the
OpenTelemetry SDK picked it up directly.

So really this is a story about testing. We weren't testing environment variable loading.
There is still work to do but now there are some tests. The test harness already had most
everything I needed to build the tests so I did that first. At least now we won't be
embarrassed again.

I'll mention the fixes to Viper for posterity as they were tricky to figure out and should
become a post of their own. The first is that viper.Unmarshal uses mapstructure under
the covers so it's the `mapstructure:"tag"` struct tag that it needs to map pflag names
to the struct fields. The second is that Viper expects JSON maps in envvars, not the
k=v,k=v format otel-cli uses elsewhere and documents. So I had to add this, which
also took a minute to figure out:
https://github.com/tobert/otel-cli/blob/109-fix-envvar-settings/otelcli/root.go#L158

After getting tests passing with Viper in a PR, I went looking at issues and read #83 
and decided to take a little detour and see what removing Viper looked like. I always
wanted to try doing struct tags by hand too so I gave that a go and that's what we're
going with for now. So this PR / squash merge is a derivative of the Viper PR in #117 
that completely removes it for custom code, and ends up shrinking the binary on
x86_64 by 1.2 megabytes. That was enough for me to polish up the removal and land
us here.

Normally I would avoid such a big behavioral change, knowing folks are using otel-cli
on their systems, but since the functionality has been broken so long, now's the time
to make the cut and clean up a little bit.

-Amy

Fixes: #83 #109

Commit history follows:

* is_sampled was never available in span data

This never should have worked. Next commit will fix the span data
checker.

* rework checkSpanData for correctness

Working on a test for #109 it became clear there were some faults in how
checkSpanData worked. For one it wasn't validating that an expected
'is_sampled' was actually in the received span. It turns out that field
was never available for comparison. That check was added.

Also only the regex fields were being checked because of looping over
that map so I moved it up to a global map and now loop over received
fields and compare against instead of the other way around. This should
be more exhaustive.

* add failing test for #109

* plumb service attributes into CliEvent

In order to test service name setting, this needs to come through in the
tests. This patch does that.

Refactored attribute flattening code to its own function.

* make a failing test for service name

This test should pass once the bug is fixed.

* make attr map flattening more ergonomic

* also test OTEL_RESOURCE_ATTRIBUTES

also test multiple attributes are handled correctly

* add mapstructure struct tags

viper seems to need these. the service name envvar works now.
OTEL_CLI_ATTRIBUTES still does not work.

* check error on viper.Unmarshal 🤦

test fails in a more interesting way now

always check your errors, kids

* add a comment

* add parseCkvStringMap helper & test

The one in pflags is buried deep so we need our own to make Viper do the
right thing.

* hack: strip types off stringified span attributes

This isn't ideal since the type information is there somewhere and it
could be converted cleanly. That said, I dug through the grpc and otel
code trying to figure it out and didn't make much progress. Since this
server isn't super serious, it should be ok to clean up with string
manipulation and get all the tests to work as expected. This seems to
work for now so proper type conversion can happen another day. Sorry.

* implement & use mapstructure decode hook

By default viper will parse JSON in envvars but not the comma-separated
k=v style that cobra/pflag does. This implements a hook to intercept
those values and decode them manually, which makes OTEL_CLI_ATTRIBUTES
finally work as advertised.

* use strings.SplitN instead of Cut for older go

For now, let's make sure folks can build widely. Probably should make a
decision soon about minimum Go compiler though.

* replace viper with custom environment loading

We discussed this in #83. It seems to be less code overall, even before
removing the dependency.

* clean up TODOs, wrap errors

* make env parse failures a soft failure

I'm not super sure this is the right move but feels like it follows
policy.

* add envvar failure test, add Results.CliOutputRe

Wrote a test to verify parsing fails on an invalid environment variable
value.

Because softFail uses log.Fatal, it includes a date stamp which is the
right thing to do. To make testing easier, this called for a way to
modify the output before comparing to the fixture data, thus I added
Results.CliOutputRe which takes a regex and always deletes the match.

* remove accidental comment

* re-implment config file loading

It's json-only now, and an example is added. I'm not thrilled about the
ordering of Defaults -> CLI -> environment -> file but it works and is
consistent.

* update README docs for configuration

* fix up config test, add needed infra for it

In order to fix the config test, a "*" option needed to be added to the
test harness so I did that. Cleaned up up README and other things
after getting the test right, so there are a few odds & ends in this
commit that are related.

* correct comment
  • Loading branch information
tobert authored Jan 11, 2023
1 parent efb5608 commit 8eb37fb
Show file tree
Hide file tree
Showing 12 changed files with 476 additions and 252 deletions.
49 changes: 28 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,34 @@ otel-cli server json --dir $dir --timeout 60 --max-spans 5

## Configuration

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

| CLI argument | environment variable | config file key | example value |
| -------------------- | ----------------------------- | ------------------ | -------------- |
| --endpoint | OTEL_EXPORTER_OTLP_ENDPOINT | endpoint | localhost:4317 |
| --insecure | OTEL_EXPORTER_OTLP_INSECURE | insecure | false |
| --timeout | OTEL_EXPORTER_OTLP_TIMEOUT | timeout | 1s |
| --otlp-headers | OTEL_EXPORTER_OTLP_HEADERS | otlp-headers | k=v,a=b |
| --otlp-blocking | OTEL_EXPORTER_OTLP_BLOCKING | otlp-blocking | false |
| --service | OTEL_CLI_SERVICE_NAME | service | myapp |
| --kind | OTEL_CLI_TRACE_KIND | kind | server |
| --status-code | OTEL_CLI_STATUS_CODE | status-code | error |
| --status-description | OTEL_CLI_STATUS_DESCRIPTION | status-description | cancelled |
| --attrs | OTEL_CLI_ATTRIBUTES | attrs | k=v,a=b |
| --tp-required | OTEL_CLI_TRACEPARENT_REQUIRED | tp-required | false |
| --tp-carrier | OTEL_CLI_CARRIER_FILE | tp-carrier | filename.txt |
| --tp-ignore-env | OTEL_CLI_IGNORE_ENV | tp-ignore-env | false |
| --tp-print | OTEL_CLI_PRINT_TRACEPARENT | tp-print | false |
| --tp-export | OTEL_CLI_EXPORT_TRACEPARENT | tp-export | false |
| --no-tls-verify | OTEL_CLI_NO_TLS_VERIFY | no-tls-verify | false |
Everything is configurable via CLI arguments, json config, and environment
variables. If no endpoint is specified, otel-cli will run in non-recording
mode and not attempt to contact any servers.

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

| CLI argument | environment variable | config file key | example value |
| -------------------- | ----------------------------- | ------------------------ | -------------- |
| --endpoint | OTEL_EXPORTER_OTLP_ENDPOINT | endpoint | localhost:4317 |
| --insecure | OTEL_EXPORTER_OTLP_INSECURE | insecure | false |
| --timeout | OTEL_EXPORTER_OTLP_TIMEOUT | timeout | 1s |
| --otlp-headers | OTEL_EXPORTER_OTLP_HEADERS | otlp_headers | k=v,a=b |
| --otlp-blocking | OTEL_EXPORTER_OTLP_BLOCKING | otlp_blocking | false |
| --config | OTEL_CLI_CONFIG_FILE | config_file | config.json |
| --verbose | OTEL_CLI_VERBOSE | verbose | false |
| --fail | OTEL_CLI_FAIL | fail | false |
| --service | OTEL_CLI_SERVICE_NAME | service_name | myapp |
| --kind | OTEL_CLI_TRACE_KIND | span_kind | server |
| --status-code | OTEL_CLI_STATUS_CODE | span_status_code | error |
| --status-description | OTEL_CLI_STATUS_DESCRIPTION | span_status_description | cancelled |
| --attrs | OTEL_CLI_ATTRIBUTES | span_attributes | k=v,a=b |
| --tp-required | OTEL_CLI_TRACEPARENT_REQUIRED | traceparent_required | false |
| --tp-carrier | OTEL_CLI_CARRIER_FILE | traceparent_carrier_file | filename.txt |
| --tp-ignore-env | OTEL_CLI_IGNORE_ENV | traceparent_ignore_env | false |
| --tp-print | OTEL_CLI_PRINT_TRACEPARENT | traceparent_print | false |
| --tp-export | OTEL_CLI_EXPORT_TRACEPARENT | traceparent_print_export | false |
| --no-tls-verify | OTEL_CLI_NO_TLS_VERIFY | no_tls_verify | false |

[Valid timeout units](https://pkg.go.dev/time#ParseDuration) are "ns", "us"/"µs", "ms", "s", "m", "h".

Expand Down
131 changes: 115 additions & 16 deletions data_for_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ package main_test
// TODO: Results.SpanData could become a struct now
// TODO: add instructions for adding more tests

import "github.com/equinix-labs/otel-cli/otelcli"
import (
"regexp"

"github.com/equinix-labs/otel-cli/otelcli"
)

type FixtureConfig struct {
CliArgs []string
Expand Down Expand Up @@ -34,11 +38,12 @@ type Results struct {
Env map[string]string `json:"env"`
Diagnostics otelcli.Diagnostics `json:"diagnostics"`
// these are specific to tests...
CliOutput string // merged stdout and stderr
Spans int // number of spans received
Events int // number of events received
TimedOut bool // true when test timed out
CommandFailed bool // otel-cli failed / was killed
CliOutput string // merged stdout and stderr
CliOutputRe *regexp.Regexp // regular expression to clean the output before comparison
Spans int // number of spans received
Events int // number of events received
TimedOut bool // true when test timed out
CommandFailed bool // otel-cli failed / was killed
}

// Fixture represents a test fixture for otel-cli.
Expand Down Expand Up @@ -101,8 +106,9 @@ var suites = []FixtureSuite{
},
},
},
// otel is configured but there is no server listening so it should time out silently
// ensure things fail when they're supposed to fail
{
// otel is configured but there is no server listening so it should time out silently
{
Name: "timeout with no server",
Config: FixtureConfig{
Expand All @@ -122,6 +128,24 @@ var suites = []FixtureSuite{
CommandFailed: true,
},
},
{
Name: "syntax errors in environment variables cause the command to fail",
Config: FixtureConfig{
CliArgs: []string{"span", "--fail", "--verbose"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_CLI_VERBOSE": "lmao", // invalid input
},
},
Expect: Results{
Config: otelcli.DefaultConfig(),
CommandFailed: true,
// strips the date off the log line before comparing to expectation
CliOutputRe: regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `),
CliOutput: "Error while loading environment variables: could not parse OTEL_CLI_VERBOSE value " +
"\"lmao\" as an bool: strconv.ParseBool: parsing \"lmao\": invalid syntax\n",
},
},
},
// otel-cli span with no OTLP config should do and print nothing
{
Expand All @@ -133,6 +157,59 @@ var suites = []FixtureSuite{
Expect: Results{Config: otelcli.DefaultConfig()},
},
},
// config file
{
{
Name: "load a json config file",
Config: FixtureConfig{
CliArgs: []string{"status", "--config", "example-config.json"},
// this will take priority over the config
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
},
TestTimeoutMs: 1000,
},
Expect: Results{
Spans: 1,
Diagnostics: otelcli.Diagnostics{
IsRecording: true,
NumArgs: 3,
ParsedTimeoutMs: 1000,
},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
},
Config: otelcli.DefaultConfig().
WithEndpoint("*"). // tells the test framework to ignore/overwrite
WithTimeout("1s").
WithHeaders(map[string]string{"header1": "header1-value"}).
WithInsecure(true).
WithBlocking(false).
WithNoTlsVerify(true).
WithServiceName("configured_in_config_file").
WithSpanName("config_file_span").
WithKind("server").
WithAttributes(map[string]string{"attr1": "value1"}).
WithStatusCode("0").
WithStatusDescription("status description").
WithTraceparentCarrierFile("/tmp/traceparent.txt").
WithTraceparentIgnoreEnv(true).
WithTraceparentPrint(true).
WithTraceparentPrintExport(true).
WithTraceparentRequired(true).
WithBackgroundParentPollMs(100).
WithBackgroundSockdir("/tmp").
WithBackgroundWait(true).
WithSpanEndTime("now").
WithSpanEndTime("now").
WithEventName("config_file_event").
WithEventTime("now").
WithCfgFile("example-config.json").
WithVerbose(true).
WithFail(true),
},
},
},
// otel-cli with minimal config span sends a span that looks right
{
{
Expand All @@ -147,9 +224,33 @@ var suites = []FixtureSuite{
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"is_sampled": "true",
"span_id": "*",
"trace_id": "*",
"span_id": "*",
"trace_id": "*",
},
Spans: 1,
},
},
// OTEL_RESOURCE_ATTRIBUTES and OTEL_CLI_SERVICE_NAME should get merged into
// the span resource attributes
{
Name: "otel-cli span with envvar service name and attributes (recording)",
Config: FixtureConfig{
CliArgs: []string{"span", "--name", "test-span-service-name-and-attrs", "--kind", "server"},
Env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "{{endpoint}}",
"OTEL_CLI_SERVICE_NAME": "test-service-abc123",
"OTEL_CLI_ATTRIBUTES": "cafe=deadbeef,abc=123",
"OTEL_RESOURCE_ATTRIBUTES": "foo.bar=baz",
},
TestTimeoutMs: 1000,
},
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"span_id": "*",
"trace_id": "*",
"attributes": "abc=123,cafe=deadbeef",
"service_attributes": "foo.bar=baz,service.name=test-service-abc123",
},
Spans: 1,
},
Expand Down Expand Up @@ -285,9 +386,8 @@ var suites = []FixtureSuite{
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"is_sampled": "true",
"span_id": "*",
"trace_id": "edededededededededededededed9000",
"span_id": "*",
"trace_id": "edededededededededededededed9000",
},
CliOutput: "hello world\n",
Spans: 1,
Expand All @@ -307,9 +407,8 @@ var suites = []FixtureSuite{
Expect: Results{
Config: otelcli.DefaultConfig(),
SpanData: map[string]string{
"is_sampled": "true",
"span_id": "*",
"trace_id": "*",
"span_id": "*",
"trace_id": "*",
},
CliOutput: "hello world\n",
Spans: 2,
Expand Down
38 changes: 38 additions & 0 deletions example-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"endpoint" : "localhost:4317",
"timeout" : "1s",
"otlp_headers" : {
"header1" : "header1-value"
},
"otlp_blocking" : false,
"insecure" : true,
"no_tls_verify" : true,

"service_name" : "configured_in_config_file",

"span_name" : "config_file_span",
"span_kind" : "server",
"span_attributes" : {
"attr1" : "value1"
},
"span_end_time" : "now",
"span_start_time" : "now",
"span_status_code" : "0",
"span_status_description" : "status description",

"event_name" : "config_file_event",
"event_time" : "now",

"background_parent_poll_ms" : 100,
"background_socket_directory" : "/tmp",
"background_wait" : true,

"traceparent_carrier_file" : "/tmp/traceparent.txt",
"traceparent_ignore_env" : true,
"traceparent_print" : true,
"traceparent_print_export" : true,
"traceparent_required" : true,

"verbose" : true,
"fail" : true
}
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ go 1.14

require (
github.com/google/go-cmp v0.5.7
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.8.1
github.com/pterm/pterm v0.12.30
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.0
go.opentelemetry.io/otel v1.4.1
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1
Expand Down
Loading

0 comments on commit 8eb37fb

Please sign in to comment.