Skip to content

Commit

Permalink
refactor Config out of otlpclient (#245)
Browse files Browse the repository at this point in the history
Moves the Config from otlpclient to otelcli, where it belongs. It only ended up in otlpclient
because in the first pass of splitting things up, it was easier to keep it with the client. With
this PR, otlpclient breaks dependence on Config by switching to an interface for configuration.
otelcli.Config implements that interface.

The vast majority of this change is moving code around, and adding a few methods along
way for interfaces.

No new features.

`otel-cli --otlp-blocking` is now a no-op and is labeled deprecated.

Squashed commitlog follows:

* move tlsConfig to its own file, rename to TlsConfig

* move TlsConfig to be a method attached to Config

* move SendSpan to using a config interface instead of struct

First step in splitting out config is adding getter methods to Config
and moving code to use an interface instead.

* move ParseEndpoint to Config, add GetEndpoint()

* refactor Config usage out of otlp client

Fills in OTLPConfig interface with the needed methods to break
dependence on Config so it can move out.

* remove unused function, add godoc to another

* move NewProtobufSpanWithConfig to Config, move to span_config.go

protobuf span is now clear of Config usage. This can move with Config
and can stay with it since it's all about translating otel-cli config to
a span and is useless otherwise.

* update broken tests

* move Config files to otelcli

* finish moving Config to otelcli.Config

* fix tests, fix comments

* rename files to make more sense

* fix comment

* remove dead code

Nothing was using the retry counter and Diag can't work in otlpclient
anymore so out it goes.
  • Loading branch information
tobert authored Jul 18, 2023
1 parent b2e0ce4 commit 9a19f5f
Show file tree
Hide file tree
Showing 28 changed files with 790 additions and 681 deletions.
146 changes: 80 additions & 66 deletions data_for_test.go

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"os"

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

// these will be set by goreleaser & ldflags at build time
Expand All @@ -16,5 +15,5 @@ var (

func main() {
otelcli.Execute(otelcli.FormatVersion(version, commit, date))
os.Exit(otlpclient.GetExitCode())
os.Exit(otelcli.GetExitCode())
}
3 changes: 1 addition & 2 deletions otelcli/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import (
"log"
"os"

"github.com/equinix-labs/otel-cli/otlpclient"
"github.com/spf13/cobra"
)

func completionCmd(config *otlpclient.Config) *cobra.Command {
func completionCmd(config *Config) *cobra.Command {
cmd := cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Expand Down
114 changes: 107 additions & 7 deletions otlpclient/config.go → otelcli/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package otlpclient
package otelcli

import (
"encoding/csv"
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"path"
"reflect"
"regexp"
"sort"
Expand Down Expand Up @@ -64,6 +66,8 @@ func DefaultConfig() Config {
Fail: false,
StatusCode: "unset",
StatusDescription: "",
Version: "unset",
StartupTime: time.Now(),
}
}

Expand Down Expand Up @@ -235,9 +239,9 @@ func (c Config) ToStringMap() map[string]string {
}
}

// IsRecording returns true if an endpoint is set and otel-cli expects to send real
// GetIsRecording returns true if an endpoint is set and otel-cli expects to send real
// spans. Returns false if unconfigured and going to run inert.
func (c Config) IsRecording() bool {
func (c Config) GetIsRecording() bool {
if c.Endpoint == "" && c.TracesEndpoint == "" {
Diag.IsRecording = false
return false
Expand Down Expand Up @@ -280,6 +284,60 @@ func parseDuration(d string) (time.Duration, error) {
return out, nil
}

// ParseEndpoint takes the endpoint or signal endpoint, augments as needed
// (e.g. bare host:port for gRPC) and then parses as a URL.
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp
func (config Config) ParseEndpoint() (*url.URL, string) {
var endpoint, source string
var epUrl *url.URL
var err error

// signal-specific configs get precedence over general endpoint per OTel spec
if config.TracesEndpoint != "" {
endpoint = config.TracesEndpoint
source = "signal"
} else if config.Endpoint != "" {
endpoint = config.Endpoint
source = "general"
} else {
config.SoftFail("no endpoint configuration available")
}

parts := strings.Split(endpoint, ":")
// bare hostname? can only be grpc, prepend
if len(parts) == 1 {
epUrl, err = url.Parse("grpc://" + endpoint + ":4317")
if err != nil {
config.SoftFail("error parsing (assumed) gRPC bare host address '%s': %s", endpoint, err)
}
} else if len(parts) > 1 { // could be URI or host:port
// actual URIs
// grpc:// is only an otel-cli thing, maybe should drop it?
if parts[0] == "grpc" || parts[0] == "http" || parts[0] == "https" {
epUrl, err = url.Parse(endpoint)
if err != nil {
config.SoftFail("error parsing provided %s URI '%s': %s", source, endpoint, err)
}
} else {
// gRPC host:port
epUrl, err = url.Parse("grpc://" + endpoint)
if err != nil {
config.SoftFail("error parsing (assumed) gRPC host:port address '%s': %s", endpoint, err)
}
}
}

// Per spec, /v1/traces is the default, appended to any url passed
// to the general endpoint
if strings.HasPrefix(epUrl.Scheme, "http") && source != "signal" && !strings.HasSuffix(epUrl.Path, "/v1/traces") {
epUrl.Path = path.Join(epUrl.Path, "/v1/traces")
}

Diag.EndpointSource = source
Diag.Endpoint = epUrl.String()
return epUrl, source
}

// SoftLog only calls through to log if otel-cli was run with the --verbose flag.
// TODO: does it make any sense to support %w? probably yes, can clean up some
// diagnostics.Error touch points.
Expand Down Expand Up @@ -371,15 +429,15 @@ func parseCkvStringMap(in string) (map[string]string, error) {
return out, nil
}

// ParsedSpanStartTime returns config.SpanStartTime as time.Time.
func (c Config) ParsedSpanStartTime() time.Time {
// ParseSpanStartTime returns config.SpanStartTime as time.Time.
func (c Config) ParseSpanStartTime() time.Time {
t, err := c.parseTime(c.SpanStartTime, "start")
c.SoftFailIfErr(err)
return t
}

// ParsedSpanEndTime returns config.SpanEndTime as time.Time.
func (c Config) ParsedSpanEndTime() time.Time {
// ParseSpanEndTime returns config.SpanEndTime as time.Time.
func (c Config) ParseSpanEndTime() time.Time {
t, err := c.parseTime(c.SpanEndTime, "end")
c.SoftFailIfErr(err)
return t
Expand Down Expand Up @@ -451,6 +509,11 @@ func (c Config) parseTime(ts, which string) (time.Time, error) {
return time.Time{}, fmt.Errorf("could not parse span %s time %q as any supported format", which, ts)
}

func (c Config) GetEndpoint() *url.URL {
ep, _ := c.ParseEndpoint()
return ep
}

// WithEndpoint returns the config with Endpoint set to the provided value.
func (c Config) WithEndpoint(with string) Config {
c.Endpoint = with
Expand All @@ -469,12 +532,22 @@ func (c Config) WithProtocol(with string) Config {
return c
}

// GetTimeout returns the parsed --timeout value as a time.Duration.
func (c Config) GetTimeout() time.Duration {
return c.ParseCliTimeout()
}

// WithTimeout returns the config with Timeout set to the provided value.
func (c Config) WithTimeout(with string) Config {
c.Timeout = with
return c
}

// GetHeaders returns the stringmap of configured headers.
func (c Config) GetHeaders() map[string]string {
return c.Headers
}

// WithHeades returns the config with Heades set to the provided value.
func (c Config) WithHeaders(with map[string]string) Config {
c.Headers = with
Expand Down Expand Up @@ -517,6 +590,11 @@ func (c Config) WithTlsClientCert(with string) Config {
return c
}

// GetServiceName returns the configured OTel service name.
func (c Config) GetServiceName() string {
return c.ServiceName
}

// WithServiceName returns the config with ServiceName set to the provided value.
func (c Config) WithServiceName(with string) Config {
c.ServiceName = with
Expand Down Expand Up @@ -660,3 +738,25 @@ func (c Config) WithFail(with bool) Config {
c.Fail = with
return c
}

// Version returns the program version stored in the config.
func (c Config) GetVersion() string {
return c.Version
}

// WithVersion returns the config with Version set to the provided value.
func (c Config) WithVersion(with string) Config {
c.Version = with
return c
}

// GetStartupTime returns the configured startup time.
func (c Config) GetStartupTime() time.Time {
return c.StartupTime
}

// WithStartupTime returns the config with StartupTime set to the provided value.
func (c Config) WithStartupTime(with time.Time) Config {
c.StartupTime = with
return c
}
146 changes: 146 additions & 0 deletions otelcli/config_span.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package otelcli

import (
"encoding/hex"
"fmt"
"io"
"time"

"github.com/equinix-labs/otel-cli/otlpclient"
"github.com/equinix-labs/otel-cli/w3c/traceparent"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
)

// NewProtobufSpan creates a new span and populates it with information
// from the config struct.
func (c Config) NewProtobufSpan() *tracepb.Span {
span := otlpclient.NewProtobufSpan()
if c.GetIsRecording() {
span.TraceId = otlpclient.GenerateTraceId()
span.SpanId = otlpclient.GenerateSpanId()
}
span.Name = c.SpanName
span.Kind = otlpclient.SpanKindStringToInt(c.Kind)
span.Attributes = otlpclient.StringMapAttrsToProtobuf(c.Attributes)

now := time.Now()
if c.SpanStartTime != "" {
st := c.ParseSpanStartTime()
span.StartTimeUnixNano = uint64(st.UnixNano())
} else {
span.StartTimeUnixNano = uint64(now.UnixNano())
}

if c.SpanEndTime != "" {
et := c.ParseSpanEndTime()
span.EndTimeUnixNano = uint64(et.UnixNano())
} else {
span.EndTimeUnixNano = uint64(now.UnixNano())
}

if c.GetIsRecording() {
tp := c.LoadTraceparent()
if tp.Initialized {
span.TraceId = tp.TraceId
span.ParentSpanId = tp.SpanId
}
} else {
span.TraceId = otlpclient.GetEmptyTraceId()
span.SpanId = otlpclient.GetEmptySpanId()
}

// --force-trace-id, --force-span-id and --force-parent-span-id let the user set their own trace, span & parent span ids
// these work in non-recording mode and will stomp trace id from the traceparent
var err error
if c.ForceTraceId != "" {
span.TraceId, err = parseHex(c.ForceTraceId, 16)
c.SoftFailIfErr(err)
}
if c.ForceSpanId != "" {
span.SpanId, err = parseHex(c.ForceSpanId, 8)
c.SoftFailIfErr(err)
}
if c.ForceParentSpanId != "" {
span.ParentSpanId, err = parseHex(c.ForceParentSpanId, 8)
c.SoftFailIfErr(err)
}

otlpclient.SetSpanStatus(span, c.StatusCode, c.StatusDescription)

return span
}

// LoadTraceparent follows otel-cli's loading rules, start with envvar then file.
// If both are set, the file will override env.
// When in non-recording mode, the previous traceparent will be returned if it's
// available, otherwise, a zero-valued traceparent is returned.
func (c Config) LoadTraceparent() traceparent.Traceparent {
tp := traceparent.Traceparent{
Version: 0,
TraceId: otlpclient.GetEmptyTraceId(),
SpanId: otlpclient.GetEmptySpanId(),
Sampling: false,
Initialized: true,
}

if !c.TraceparentIgnoreEnv {
var err error
tp, err = traceparent.LoadFromEnv()
if err != nil {
Diag.Error = err.Error()
}
}

if c.TraceparentCarrierFile != "" {
fileTp, err := traceparent.LoadFromFile(c.TraceparentCarrierFile)
if err != nil {
Diag.Error = err.Error()
} else if fileTp.Initialized {
tp = fileTp
}
}

if c.TraceparentRequired {
if tp.Initialized {
return tp
} else {
c.SoftFail("failed to find a valid traceparent carrier in either environment for file '%s' while it's required by --tp-required", c.TraceparentCarrierFile)
}
}

return tp
}

// PropagateTraceparent saves the traceparent to file if necessary, then prints
// span info to the console according to command-line args.
func (c Config) PropagateTraceparent(span *tracepb.Span, target io.Writer) {
var tp traceparent.Traceparent
if c.GetIsRecording() {
tp = otlpclient.TraceparentFromProtobufSpan(span, c.GetIsRecording())
} else {
// when in non-recording mode, and there is a TP available, propagate that
tp = c.LoadTraceparent()
}

if c.TraceparentCarrierFile != "" {
err := tp.SaveToFile(c.TraceparentCarrierFile, c.TraceparentPrintExport)
c.SoftFailIfErr(err)
}

if c.TraceparentPrint {
tp.Fprint(target, c.TraceparentPrintExport)
}
}

// parseHex parses hex into a []byte of length provided. Errors if the input is
// not valid hex or the converted hex is not the right number of bytes.
func parseHex(in string, expectedLen int) ([]byte, error) {
out, err := hex.DecodeString(in)
if err != nil {
return nil, fmt.Errorf("error parsing hex string %q: %w", in, err)
}
if len(out) != expectedLen {
return nil, fmt.Errorf("hex string %q is the wrong length, expected %d bytes but got %d", in, expectedLen, len(out))
}
return out, nil
}
Loading

0 comments on commit 9a19f5f

Please sign in to comment.