diff --git a/README.md b/README.md index de215ea..eefb228 100644 --- a/README.md +++ b/README.md @@ -96,22 +96,24 @@ otel-cli server json --dir $dir --timeout 60 --max-spans 5 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 | -| --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 | +| 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 | [Valid timeout units](https://pkg.go.dev/time#ParseDuration) are "ns", "us"/"µs", "ms", "s", "m", "h". diff --git a/otelcli/config.go b/otelcli/config.go index 5f4da52..255a16e 100644 --- a/otelcli/config.go +++ b/otelcli/config.go @@ -43,6 +43,8 @@ func DefaultConfig() Config { CfgFile: "", Verbose: false, Fail: false, + StatusCode: "unset", + StatusDescription: "", } } @@ -57,10 +59,12 @@ type Config struct { Blocking bool `json:"blocking"` NoTlsVerify bool `json:"no_tls_verify"` - ServiceName string `json:"service_name"` - SpanName string `json:"span_name"` - Kind string `json:"span_kind"` - Attributes map[string]string `json:"span_attributes"` + ServiceName string `json:"service_name"` + SpanName string `json:"span_name"` + Kind string `json:"span_kind"` + Attributes map[string]string `json:"span_attributes"` + StatusCode string `json:"span_status_code"` + StatusDescription string `json:"span_status_description"` TraceparentCarrierFile string `json:"traceparent_carrier_file"` TraceparentIgnoreEnv bool `json:"traceparent_ignore_env"` @@ -109,6 +113,8 @@ func (c Config) ToStringMap() map[string]string { "span_name": c.SpanName, "span_kind": c.Kind, "span_attributes": flattenStringMap(c.Attributes, "{}"), + "span_status_code": c.StatusCode, + "span_status_description": c.StatusDescription, "traceparent_carrier_file": c.TraceparentCarrierFile, "traceparent_ignore_env": strconv.FormatBool(c.TraceparentIgnoreEnv), "traceparent_print": strconv.FormatBool(c.TraceparentPrint), @@ -186,6 +192,18 @@ func (c Config) WithAttributes(with map[string]string) Config { return c } +// WithStatusCode returns the config with StatusCode set to the provided value. +func (c Config) WithStatusCode(with string) Config { + c.StatusCode = with + return c +} + +// WithStatusDescription returns the config with StatusDescription set to the provided value. +func (c Config) WithStatusDescription(with string) Config { + c.StatusDescription = with + return c +} + // WithTraceparentCarrierFile returns the config with TraceparentCarrierFile set to the provided value. func (c Config) WithTraceparentCarrierFile(with string) Config { c.TraceparentCarrierFile = with diff --git a/otelcli/config_test.go b/otelcli/config_test.go index dae13b2..a01c0ab 100644 --- a/otelcli/config_test.go +++ b/otelcli/config_test.go @@ -79,6 +79,27 @@ func TestWithAttributes(t *testing.T) { t.Errorf("Attributes did not match (-want +got):\n%s", diff) } } + +func TestWithStatusCode(t *testing.T) { + if DefaultConfig().WithStatusCode("unset").StatusCode != "unset" { + t.Fail() + } + + if DefaultConfig().WithStatusCode("ok").StatusCode != "ok" { + t.Fail() + } + + if DefaultConfig().WithStatusCode("error").StatusCode != "ok" { + t.Fail() + } +} + +func TestWithStatusDescription(t *testing.T) { + if DefaultConfig().WithStatusDescription("Set SCE To AUX").StatusCode != "Set SCE To AUX" { + t.Fail() + } +} + func TestWithTraceparentCarrierFile(t *testing.T) { if DefaultConfig().WithTraceparentCarrierFile("foobar").TraceparentCarrierFile != "foobar" { t.Fail() diff --git a/otelcli/helpers.go b/otelcli/helpers.go index aa99e4e..ed7d981 100644 --- a/otelcli/helpers.go +++ b/otelcli/helpers.go @@ -13,6 +13,7 @@ import ( "time" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) @@ -131,6 +132,22 @@ func otelSpanKind(kind string) trace.SpanKind { } } +// otelSpanStatus takes a supported string span status and returns the otel +// constant for it. Returns default of Unset on no match. +// TODO: figure out the best way to report invalid values +func otelSpanStatus(status string) codes.Code { + switch status { + case "unset": + return codes.Unset + case "ok": + return codes.Ok + case "error": + return codes.Error + default: + return codes.Unset + } +} + // propagateOtelCliSpan saves the traceparent to file if necessary, then prints // span info to the console according to command-line args. func propagateOtelCliSpan(ctx context.Context, span trace.Span, target io.Writer) { diff --git a/otelcli/helpers_test.go b/otelcli/helpers_test.go index fb9092d..87d8e8b 100644 --- a/otelcli/helpers_test.go +++ b/otelcli/helpers_test.go @@ -9,6 +9,7 @@ import ( "time" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) @@ -164,6 +165,38 @@ func TestOtelSpanKind(t *testing.T) { } } +func TestOtelSpanStatus(t *testing.T) { + + for _, testcase := range []struct { + name string + want codes.Code + }{ + { + name: "unset", + want: codes.Unset, + }, + { + name: "ok", + want: codes.Ok, + }, + { + name: "error", + want: codes.Error, + }, + { + name: "cromulent", + want: codes.Unset, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + out := otelSpanStatus(testcase.name) + if out != testcase.want { + t.Errorf("otelSpanStatus returned the wrong value, '%q', for '%s'", out, testcase.name) + } + }) + } +} + func TestPropagateOtelCliSpan(t *testing.T) { // TODO: should this noop the tracing backend? diff --git a/otelcli/root.go b/otelcli/root.go index 66e6ecb..e1b48d2 100644 --- a/otelcli/root.go +++ b/otelcli/root.go @@ -107,9 +107,15 @@ func addSpanParams(cmd *cobra.Command) { cmd.Flags().StringVarP(&config.ServiceName, "service", "n", defaults.ServiceName, "set the name of the application sent on the traces") // --kind / -k cmd.Flags().StringVarP(&config.Kind, "kind", "k", defaults.Kind, "set the trace kind, e.g. internal, server, client, producer, consumer") + // --status-code / -sc + cmd.Flags().StringVar(&config.StatusCode, "status-code", defaults.StatusCode, "set the span status code, e.g. unset|ok|error") + // --status-description / -sd + cmd.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'") var span_env_flags = map[string]string{ - "service": "OTEL_CLI_SERVICE_NAME", - "kind": "OTEL_CLI_TRACE_KIND", + "service": "OTEL_CLI_SERVICE_NAME", + "kind": "OTEL_CLI_TRACE_KIND", + "status-code": "OTEL_CLI_STATUS_CODE", + "status-description": "OTEL_CLI_STATUS_DESCRIPTION", } for config_key, env_value := range span_env_flags { viper.BindPFlag(config_key, cmd.Flags().Lookup(config_key)) diff --git a/otelcli/span.go b/otelcli/span.go index 6efade8..2e8c1a7 100644 --- a/otelcli/span.go +++ b/otelcli/span.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) @@ -74,6 +75,16 @@ func startSpan() (context.Context, trace.Span, func()) { ctx, span := tracer.Start(ctx, config.SpanName, startOpts...) span.SetAttributes(cliAttrsToOtel(config.Attributes)...) // applies CLI attributes to the span + spanStatus := otelSpanStatus(config.StatusCode) + + // Only set status description when an error status. + // https://github.com/open-telemetry/opentelemetry-specification/blob/480a19d702470563d32a870932be5ddae798079c/specification/trace/api.md#set-status + if spanStatus == codes.Error { + span.SetStatus(spanStatus, config.StatusDescription) + } else { + span.SetStatus(spanStatus, "") + } + return ctx, span, shutdown }