From a978450cda8f3743a24de1b21ebf35f5a9507282 Mon Sep 17 00:00:00 2001 From: Martin Hickey Date: Tue, 30 Aug 2022 20:56:12 +0100 Subject: [PATCH] [exporter/instana] Add implementation (#13620) * Add Instana exporter implementation Signed-off-by: Martin Hickey * Add unit tests Signed-off-by: Martin Hickey * Add changelog Signed-off-by: Martin Hickey * Add more unit tests Signed-off-by: Martin Hickey * Update fater review Review comments: - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#pullrequestreview-1085527736 Signed-off-by: Martin Hickey * Update after review Review comments: - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957596160 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957603426 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957603426 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957609409 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957610214 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957612370 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957613538 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957626028 - https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/13620#discussion_r957629599 Signed-off-by: Martin Hickey Signed-off-by: Martin Hickey --- exporter/instanaexporter/config.go | 5 + exporter/instanaexporter/config_test.go | 12 +- exporter/instanaexporter/exporter.go | 137 ++++++++ exporter/instanaexporter/exporter_test.go | 95 +++++ exporter/instanaexporter/factory.go | 17 +- exporter/instanaexporter/go.mod | 5 +- exporter/instanaexporter/go.sum | 2 + .../internal/backend/config.go | 25 ++ .../internal/converter/all_converter.go | 69 ++++ .../internal/converter/converter.go | 28 ++ .../internal/converter/model/bundle.go | 38 ++ .../internal/converter/model/span.go | 147 ++++++++ .../internal/converter/model/util.go | 73 ++++ .../internal/converter/model/util_test.go | 28 ++ .../internal/converter/span_converter.go | 89 +++++ .../internal/converter/span_converter_test.go | 266 ++++++++++++++ .../internal/otlptext/databuffer.go | 328 ++++++++++++++++++ .../internal/otlptext/traces.go | 63 ++++ .../internal/testutils/test_utils.go | 43 +++ unreleased/feat_add-instana-exp-impl.yaml | 16 + 20 files changed, 1469 insertions(+), 17 deletions(-) create mode 100644 exporter/instanaexporter/exporter.go create mode 100644 exporter/instanaexporter/exporter_test.go create mode 100644 exporter/instanaexporter/internal/backend/config.go create mode 100644 exporter/instanaexporter/internal/converter/all_converter.go create mode 100644 exporter/instanaexporter/internal/converter/converter.go create mode 100644 exporter/instanaexporter/internal/converter/model/bundle.go create mode 100644 exporter/instanaexporter/internal/converter/model/span.go create mode 100644 exporter/instanaexporter/internal/converter/model/util.go create mode 100644 exporter/instanaexporter/internal/converter/model/util_test.go create mode 100644 exporter/instanaexporter/internal/converter/span_converter.go create mode 100644 exporter/instanaexporter/internal/converter/span_converter_test.go create mode 100644 exporter/instanaexporter/internal/otlptext/databuffer.go create mode 100644 exporter/instanaexporter/internal/otlptext/traces.go create mode 100644 exporter/instanaexporter/internal/testutils/test_utils.go create mode 100755 unreleased/feat_add-instana-exp-impl.yaml diff --git a/exporter/instanaexporter/config.go b/exporter/instanaexporter/config.go index e0acbe37b380..73f6d5f8f616 100644 --- a/exporter/instanaexporter/config.go +++ b/exporter/instanaexporter/config.go @@ -16,6 +16,7 @@ package instanaexporter // import "github.com/open-telemetry/opentelemetry-colle import ( "errors" + "net/url" "strings" "go.opentelemetry.io/collector/config" @@ -49,6 +50,10 @@ func (cfg *Config) Validate() error { if !(strings.HasPrefix(cfg.Endpoint, "http://") || strings.HasPrefix(cfg.Endpoint, "https://")) { return errors.New("endpoint must start with http:// or https://") } + _, err := url.Parse(cfg.Endpoint) + if err != nil { + return errors.New("endpoint must be a valid URL") + } return nil } diff --git a/exporter/instanaexporter/config_test.go b/exporter/instanaexporter/config_test.go index 418de291b72b..608c478d646f 100644 --- a/exporter/instanaexporter/config_test.go +++ b/exporter/instanaexporter/config_test.go @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestConfig_Validate(t *testing.T) { +func TestConfigValidate(t *testing.T) { t.Run("Empty configuration", func(t *testing.T) { c := &Config{} err := c.Validate() @@ -35,7 +35,15 @@ func TestConfig_Validate(t *testing.T) { assert.Equal(t, "http://example.com/", c.Endpoint, "no Instana endpoint set") }) - t.Run("Invalid Endpoint", func(t *testing.T) { + t.Run("Invalid Endpoint Invalid URL", func(t *testing.T) { + c := &Config{Endpoint: "http://example.}~", AgentKey: "key1"} + err := c.Validate() + assert.Error(t, err) + + assert.Equal(t, "http://example.}~", c.Endpoint, "endpoint must be a valid URL") + }) + + t.Run("Invalid Endpoint No Protocol", func(t *testing.T) { c := &Config{Endpoint: "example.com", AgentKey: "key1"} err := c.Validate() assert.Error(t, err) diff --git a/exporter/instanaexporter/exporter.go b/exporter/instanaexporter/exporter.go new file mode 100644 index 000000000000..3d63f560c09e --- /dev/null +++ b/exporter/instanaexporter/exporter.go @@ -0,0 +1,137 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package instanaexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter" + +import ( + "bytes" + "context" + "fmt" + "net/http" + "runtime" + "strings" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config" + "go.opentelemetry.io/collector/consumer/consumererror" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/backend" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" +) + +type instanaExporter struct { + config *Config + client *http.Client + settings component.TelemetrySettings + userAgent string +} + +func (e *instanaExporter) start(_ context.Context, host component.Host) error { + client, err := e.config.HTTPClientSettings.ToClient(host, e.settings) + if err != nil { + return err + } + e.client = client + return nil +} + +func (e *instanaExporter) pushConvertedTraces(ctx context.Context, td ptrace.Traces) error { + converter := converter.NewConvertAllConverter(e.settings.Logger) + spans := make([]model.Span, 0) + + hostID := "" + resourceSpans := td.ResourceSpans() + for i := 0; i < resourceSpans.Len(); i++ { + resSpan := resourceSpans.At(i) + + resource := resSpan.Resource() + + hostIDAttr, ok := resource.Attributes().Get(backend.AttributeInstanaHostID) + if ok { + hostID = hostIDAttr.StringVal() + } + + ilSpans := resSpan.ScopeSpans() + for j := 0; j < ilSpans.Len(); j++ { + converterBundle := converter.ConvertSpans(resource.Attributes(), ilSpans.At(j).Spans()) + + spans = append(spans, converterBundle.Spans...) + } + } + + bundle := model.Bundle{Spans: spans} + if len(bundle.Spans) == 0 { + // skip exporting, nothing to do + return nil + } + + req, err := bundle.Marshal() + if err != nil { + return consumererror.NewPermanent(err) + } + + headers := map[string]string{ + backend.HeaderKey: e.config.AgentKey, + backend.HeaderHost: hostID, + // Used only by the Instana agent and can be set to "0" for the exporter + backend.HeaderTime: "0", + } + + return e.export(ctx, e.config.Endpoint, headers, req) +} + +func newInstanaExporter(cfg config.Exporter, set component.ExporterCreateSettings) *instanaExporter { + iCfg := cfg.(*Config) + userAgent := fmt.Sprintf("%s/%s (%s/%s)", set.BuildInfo.Description, set.BuildInfo.Version, runtime.GOOS, runtime.GOARCH) + return &instanaExporter{ + config: iCfg, + settings: set.TelemetrySettings, + userAgent: userAgent, + } +} + +func (e *instanaExporter) export(ctx context.Context, url string, header map[string]string, request []byte) error { + url = strings.TrimSuffix(url, "/") + "/bundle" + e.settings.Logger.Debug("Preparing to make HTTP request", zap.String("url", url)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(request)) + if err != nil { + return consumererror.NewPermanent(err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", e.userAgent) + + for name, value := range header { + req.Header.Set(name, value) + } + + resp, err := e.client.Do(req) + if err != nil { + return fmt.Errorf("failed to send a request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 && resp.StatusCode <= 499 { + return consumererror.NewPermanent(fmt.Errorf("error when sending payload to %s: %s", + url, resp.Status)) + } + if resp.StatusCode >= 500 && resp.StatusCode <= 599 { + return fmt.Errorf("error when sending payload to %s: %s", url, resp.Status) + } + + return nil +} diff --git a/exporter/instanaexporter/exporter_test.go b/exporter/instanaexporter/exporter_test.go new file mode 100644 index 000000000000..b43e5b70530a --- /dev/null +++ b/exporter/instanaexporter/exporter_test.go @@ -0,0 +1,95 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package instanaexporter + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/testutils" +) + +func TestPushConvertedDefaultTraces(t *testing.T) { + traceServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusAccepted) + })) + defer traceServer.Close() + + cfg := Config{ + AgentKey: "key11", + HTTPClientSettings: confighttp.HTTPClientSettings{Endpoint: traceServer.URL}, + Endpoint: traceServer.URL, + ExporterSettings: config.NewExporterSettings(config.NewComponentIDWithName(typeStr, "valid")), + } + + instanaExporter := newInstanaExporter(&cfg, componenttest.NewNopExporterCreateSettings()) + ctx := context.Background() + err := instanaExporter.start(ctx, componenttest.NewNopHost()) + assert.NoError(t, err) + + err = instanaExporter.pushConvertedTraces(ctx, testutils.TestTraces.Clone()) + assert.NoError(t, err) +} + +func TestPushConvertedSimpleTraces(t *testing.T) { + traceServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusAccepted) + })) + defer traceServer.Close() + + cfg := Config{ + AgentKey: "key11", + HTTPClientSettings: confighttp.HTTPClientSettings{Endpoint: traceServer.URL}, + Endpoint: traceServer.URL, + ExporterSettings: config.NewExporterSettings(config.NewComponentIDWithName(typeStr, "valid")), + } + + instanaExporter := newInstanaExporter(&cfg, componenttest.NewNopExporterCreateSettings()) + ctx := context.Background() + err := instanaExporter.start(ctx, componenttest.NewNopHost()) + assert.NoError(t, err) + + err = instanaExporter.pushConvertedTraces(ctx, simpleTraces()) + assert.NoError(t, err) +} + +func simpleTraces() ptrace.Traces { + return genTraces(pcommon.NewTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}), nil) +} + +func genTraces(traceID pcommon.TraceID, attrs map[string]interface{}) ptrace.Traces { + traces := ptrace.NewTraces() + rspans := traces.ResourceSpans().AppendEmpty() + span := rspans.ScopeSpans().AppendEmpty().Spans().AppendEmpty() + span.SetTraceID(traceID) + span.SetSpanID(pcommon.NewSpanID([8]byte{0, 0, 0, 0, 1, 2, 3, 4})) + if attrs == nil { + return traces + } + pcommon.NewMapFromRaw(attrs).Range(func(k string, v pcommon.Value) bool { + rspans.Resource().Attributes().Insert(k, v) + return true + }) + return traces +} diff --git a/exporter/instanaexporter/factory.go b/exporter/instanaexporter/factory.go index 7e065727839a..194e432c2cbe 100644 --- a/exporter/instanaexporter/factory.go +++ b/exporter/instanaexporter/factory.go @@ -56,28 +56,19 @@ func createDefaultConfig() config.Exporter { // createTracesExporter creates a trace exporter based on this configuration func createTracesExporter(ctx context.Context, set component.ExporterCreateSettings, config config.Exporter) (component.TracesExporter, error) { - // TODO: Lines commented out until implementation is available - // cfg := config.(*Config) + cfg := config.(*Config) ctx, cancel := context.WithCancel(ctx) - // TODO: Lines commented out until implementation is available - var pushConvertedTraces consumer.ConsumeTracesFunc - /*instanaExporter, err := newInstanaExporter(cfg, set) - if err != nil { - cancel() - return nil, err - }*/ + instanaExporter := newInstanaExporter(cfg, set) - //TODO: Lines commented out until implementation is available return exporterhelper.NewTracesExporter( ctx, set, config, - // instanaExporter.pushConvertedTraces, - pushConvertedTraces, + instanaExporter.pushConvertedTraces, exporterhelper.WithCapabilities(consumer.Capabilities{MutatesData: false}), - // exporterhelper.WithStart(instanaExporter.start), + exporterhelper.WithStart(instanaExporter.start), // Disable Timeout/RetryOnFailure and SendingQueue exporterhelper.WithTimeout(exporterhelper.TimeoutSettings{Timeout: 0}), exporterhelper.WithRetry(exporterhelper.RetrySettings{Enabled: false}), diff --git a/exporter/instanaexporter/go.mod b/exporter/instanaexporter/go.mod index f029a70866d1..a087efcda060 100644 --- a/exporter/instanaexporter/go.mod +++ b/exporter/instanaexporter/go.mod @@ -5,6 +5,9 @@ go 1.18 require ( github.com/stretchr/testify v1.8.0 go.opentelemetry.io/collector v0.58.1-0.20220829231818-9ba35cd40b46 + go.opentelemetry.io/collector/pdata v0.58.1-0.20220829231818-9ba35cd40b46 + go.opentelemetry.io/collector/semconv v0.58.1-0.20220829231818-9ba35cd40b46 + go.uber.org/zap v1.23.0 ) require ( @@ -27,14 +30,12 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.8.2 // indirect go.opencensus.io v0.23.0 // indirect - go.opentelemetry.io/collector/pdata v0.58.1-0.20220829231818-9ba35cd40b46 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 // indirect go.opentelemetry.io/otel v1.9.0 // indirect go.opentelemetry.io/otel/metric v0.31.0 // indirect go.opentelemetry.io/otel/trace v1.9.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/exporter/instanaexporter/go.sum b/exporter/instanaexporter/go.sum index a3e43c08226d..a5db6cd3799f 100644 --- a/exporter/instanaexporter/go.sum +++ b/exporter/instanaexporter/go.sum @@ -280,6 +280,8 @@ go.opentelemetry.io/collector v0.58.1-0.20220829231818-9ba35cd40b46 h1:Q2TUIEZK5 go.opentelemetry.io/collector v0.58.1-0.20220829231818-9ba35cd40b46/go.mod h1:c9tBjzSYfi8hUNSKbBJn1ct0rS7zG+8YcTzqsPQx2BQ= go.opentelemetry.io/collector/pdata v0.58.1-0.20220829231818-9ba35cd40b46 h1:0DGhxF/Wy+JB1czPVHBZi4+JzPJ0ROyAiT4g6JyaEWY= go.opentelemetry.io/collector/pdata v0.58.1-0.20220829231818-9ba35cd40b46/go.mod h1:0hqgNMRneVXaLNelv3q0XKJbyBW9aMDwyC15pKd30+E= +go.opentelemetry.io/collector/semconv v0.58.1-0.20220829231818-9ba35cd40b46 h1:ZBTRGXukVHpbTJ1JvPMl0hYy8h8qqY9Z/BXOLIBiq08= +go.opentelemetry.io/collector/semconv v0.58.1-0.20220829231818-9ba35cd40b46/go.mod h1:aRkHuJ/OshtDFYluKEtnG5nkKTsy1HZuvZVHmakx+Vo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 h1:9NkMW03wwEzPtP/KciZ4Ozu/Uz5ZA7kfqXJIObnrjGU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0/go.mod h1:548ZsYzmT4PL4zWKRd8q/N4z0Wxzn/ZxUE+lkEpwWQA= go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw= diff --git a/exporter/instanaexporter/internal/backend/config.go b/exporter/instanaexporter/internal/backend/config.go new file mode 100644 index 000000000000..c6e2b7e65bf5 --- /dev/null +++ b/exporter/instanaexporter/internal/backend/config.go @@ -0,0 +1,25 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backend // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/backend" + +const ( + // AttributeInstanaHostID can be used to distinguish multiple hosts' data + // being processed by a single collector (in a chained scenario) + AttributeInstanaHostID = "instana.host.id" + + HeaderKey = "x-instana-key" + HeaderHost = "x-instana-host" + HeaderTime = "x-instana-time" +) diff --git a/exporter/instanaexporter/internal/converter/all_converter.go b/exporter/instanaexporter/internal/converter/all_converter.go new file mode 100644 index 000000000000..3062c5b8835d --- /dev/null +++ b/exporter/instanaexporter/internal/converter/all_converter.go @@ -0,0 +1,69 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package converter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter" + +import ( + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" +) + +var _ Converter = (*ConvertAllConverter)(nil) + +type ConvertAllConverter struct { + converters []Converter + logger *zap.Logger +} + +func (c *ConvertAllConverter) AcceptsSpans(attributes pcommon.Map, spanSlice ptrace.SpanSlice) bool { + return true +} + +func (c *ConvertAllConverter) ConvertSpans(attributes pcommon.Map, spanSlice ptrace.SpanSlice) model.Bundle { + bundle := model.NewBundle() + + for i := 0; i < len(c.converters); i++ { + if !c.converters[i].AcceptsSpans(attributes, spanSlice) { + c.logger.Warn(fmt.Sprintf("Converter %q didn't accept spans", c.converters[i].Name())) + + continue + } + + converterBundle := c.converters[i].ConvertSpans(attributes, spanSlice) + if len(converterBundle.Spans) > 0 { + bundle.Spans = append(bundle.Spans, converterBundle.Spans...) + } + } + + return bundle +} + +func (c *ConvertAllConverter) Name() string { + return "ConvertAllConverter" +} + +func NewConvertAllConverter(logger *zap.Logger) Converter { + + return &ConvertAllConverter{ + converters: []Converter{ + &SpanConverter{logger: logger}, + }, + logger: logger, + } +} diff --git a/exporter/instanaexporter/internal/converter/converter.go b/exporter/instanaexporter/internal/converter/converter.go new file mode 100644 index 000000000000..54c7c76a1630 --- /dev/null +++ b/exporter/instanaexporter/internal/converter/converter.go @@ -0,0 +1,28 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package converter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter" + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" +) + +type Converter interface { + AcceptsSpans(attributes pcommon.Map, spanSlice ptrace.SpanSlice) bool + ConvertSpans(attributes pcommon.Map, spanSlice ptrace.SpanSlice) model.Bundle + Name() string +} diff --git a/exporter/instanaexporter/internal/converter/model/bundle.go b/exporter/instanaexporter/internal/converter/model/bundle.go new file mode 100644 index 000000000000..d5b89125c287 --- /dev/null +++ b/exporter/instanaexporter/internal/converter/model/bundle.go @@ -0,0 +1,38 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" + +import ( + "encoding/json" +) + +type Bundle struct { + Spans []Span `json:"spans,omitempty"` +} + +func NewBundle() Bundle { + return Bundle{ + Spans: make([]Span, 0), + } +} + +func (b *Bundle) Marshal() ([]byte, error) { + json, err := json.Marshal(b) + if err != nil { + return nil, err + } + + return json, nil +} diff --git a/exporter/instanaexporter/internal/converter/model/span.go b/exporter/instanaexporter/internal/converter/model/span.go new file mode 100644 index 000000000000..b51e08a3c29f --- /dev/null +++ b/exporter/instanaexporter/internal/converter/model/span.go @@ -0,0 +1,147 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" + +import ( + "fmt" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +const ( + OtelSpanType = "otel" + + InstanaSpanKindServer = "server" + InstanaSpanKindClient = "client" + InstanaSpanKindProducer = "producer" + InstanaSpanKindConsumer = "consumer" + InstanaSpanKindInternal = "internal" + + InstanaDataService = "service" + InstanaDataOperation = "operation" + InstanaDataTraceState = "trace_state" + InstanaDataError = "error" + InstanaDataErrorDetail = "error_detail" +) + +type BatchInfo struct { + Size int `json:"s"` +} + +type FromS struct { + EntityID string `json:"e"` + // Serverless agents fields + Hostless bool `json:"hl,omitempty"` + CloudProvider string `json:"cp,omitempty"` + // Host agent fields + HostID string `json:"h,omitempty"` +} + +type TraceReference struct { + TraceID string `json:"t"` + ParentID string `json:"p,omitempty"` +} + +type OTelSpanData struct { + Kind string `json:"kind"` + HasTraceParent bool `json:"tp,omitempty"` + ServiceName string `json:"service"` + Operation string `json:"operation"` + TraceState string `json:"trace_state,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +type Span struct { + TraceReference + + SpanID string `json:"s"` + LongTraceID string `json:"lt,omitempty"` + Timestamp uint64 `json:"ts"` + Duration uint64 `json:"d"` + Name string `json:"n"` + From *FromS `json:"f"` + Batch *BatchInfo `json:"b,omitempty"` + Ec int `json:"ec,omitempty"` + Synthetic bool `json:"sy,omitempty"` + CorrelationType string `json:"crtp,omitempty"` + CorrelationID string `json:"crid,omitempty"` + ForeignTrace bool `json:"tp,omitempty"` + Ancestor *TraceReference `json:"ia,omitempty"` + Data OTelSpanData `json:"data,omitempty"` +} + +func ConvertPDataSpanToInstanaSpan(fromS FromS, otelSpan ptrace.Span, serviceName string, attributes pcommon.Map) (Span, error) { + traceID := convertTraceID(otelSpan.TraceID()) + + instanaSpan := Span{ + Name: OtelSpanType, + TraceReference: TraceReference{}, + Timestamp: uint64(otelSpan.StartTimestamp()) / uint64(time.Millisecond), + Duration: (uint64(otelSpan.EndTimestamp()) - uint64(otelSpan.StartTimestamp())) / uint64(time.Millisecond), + Data: OTelSpanData{ + Tags: make(map[string]string), + }, + From: &fromS, + } + + if len(traceID) != 32 { + return Span{}, fmt.Errorf("failed parsing span, length of TraceID should be 32, but got %d", len(traceID)) + } + + instanaSpan.TraceReference.TraceID = traceID[16:32] + instanaSpan.LongTraceID = traceID + + if !otelSpan.ParentSpanID().IsEmpty() { + instanaSpan.TraceReference.ParentID = convertSpanID(otelSpan.ParentSpanID()) + } + + instanaSpan.SpanID = convertSpanID(otelSpan.SpanID()) + + kind, isEntry := otelKindToInstanaKind(otelSpan.Kind()) + instanaSpan.Data.Kind = kind + + if !otelSpan.ParentSpanID().IsEmpty() && isEntry { + instanaSpan.Data.HasTraceParent = true + } + + instanaSpan.Data.ServiceName = serviceName + + instanaSpan.Data.Operation = otelSpan.Name() + + if otelSpan.TraceState() != ptrace.TraceStateEmpty { + instanaSpan.Data.TraceState = string(otelSpan.TraceState()) + } + + otelSpan.Attributes().Sort().Range(func(k string, v pcommon.Value) bool { + instanaSpan.Data.Tags[k] = v.AsString() + + return true + }) + + errornous := false + if otelSpan.Status().Code() == ptrace.StatusCodeError { + errornous = true + instanaSpan.Data.Tags[InstanaDataError] = otelSpan.Status().Code().String() + instanaSpan.Data.Tags[InstanaDataErrorDetail] = otelSpan.Status().Message() + } + + if errornous { + instanaSpan.Ec = 1 + } + + return instanaSpan, nil +} diff --git a/exporter/instanaexporter/internal/converter/model/util.go b/exporter/instanaexporter/internal/converter/model/util.go new file mode 100644 index 000000000000..5f4e88aacc01 --- /dev/null +++ b/exporter/instanaexporter/internal/converter/model/util.go @@ -0,0 +1,73 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" + +import ( + "encoding/hex" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +func convertTraceID(traceID pcommon.TraceID) string { + const byteLength = 16 + + bytes := traceID.Bytes() + traceBytes := make([]byte, 0) + + for (len(traceBytes) + len(bytes)) < byteLength { + traceBytes = append(traceBytes, 0) + } + + for _, byte := range bytes { + traceBytes = append(traceBytes, byte) + } + + return hex.EncodeToString(traceBytes) +} + +func convertSpanID(spanID pcommon.SpanID) string { + const byteLength = 8 + + bytes := spanID.Bytes() + spanBytes := make([]byte, 0) + + for (len(spanBytes) + len(bytes)) < byteLength { + spanBytes = append(spanBytes, 0) + } + + for _, byte := range bytes { + spanBytes = append(spanBytes, byte) + } + + return hex.EncodeToString(spanBytes) +} + +func otelKindToInstanaKind(otelKind ptrace.SpanKind) (string, bool) { + switch otelKind { + case ptrace.SpanKindServer: + return InstanaSpanKindServer, true + case ptrace.SpanKindClient: + return InstanaSpanKindClient, false + case ptrace.SpanKindProducer: + return InstanaSpanKindProducer, false + case ptrace.SpanKindConsumer: + return InstanaSpanKindConsumer, true + case ptrace.SpanKindInternal: + return InstanaSpanKindInternal, false + default: + return "unknown", false + } +} diff --git a/exporter/instanaexporter/internal/converter/model/util_test.go b/exporter/instanaexporter/internal/converter/model/util_test.go new file mode 100644 index 000000000000..4c10c2eaa720 --- /dev/null +++ b/exporter/instanaexporter/internal/converter/model/util_test.go @@ -0,0 +1,28 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" +) + +func TestCanConvertSpanID(t *testing.T) { + bytes := [8]byte{1, 2, 3, 4, 10, 11, 12, 13} + + assert.Equal(t, "010203040a0b0c0d", convertSpanID(pcommon.NewSpanID(bytes))) +} diff --git a/exporter/instanaexporter/internal/converter/span_converter.go b/exporter/instanaexporter/internal/converter/span_converter.go new file mode 100644 index 000000000000..0a441a33e470 --- /dev/null +++ b/exporter/instanaexporter/internal/converter/span_converter.go @@ -0,0 +1,89 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package converter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter" + +import ( + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + conventions "go.opentelemetry.io/collector/semconv/v1.8.0" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/backend" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" +) + +const ( + OtelSpanType = "otel" +) + +var _ Converter = (*SpanConverter)(nil) + +type SpanConverter struct { + logger *zap.Logger +} + +func (c *SpanConverter) AcceptsSpans(attributes pcommon.Map, spanSlice ptrace.SpanSlice) bool { + + return true +} + +func (c *SpanConverter) ConvertSpans(attributes pcommon.Map, spanSlice ptrace.SpanSlice) model.Bundle { + bundle := model.NewBundle() + spans := make([]model.Span, 0) + + fromS := model.FromS{} + + hostIDValue, ex := attributes.Get(backend.AttributeInstanaHostID) + if !ex { + fromS.HostID = "unknown-host-id" + } else { + fromS.HostID = hostIDValue.AsString() + } + + processIDValue, ex := attributes.Get(conventions.AttributeProcessPID) + if !ex { + fromS.EntityID = "unknown-process-id" + } else { + fromS.EntityID = processIDValue.AsString() + } + + serviceName := "" + serviceNameValue, ex := attributes.Get(conventions.AttributeServiceName) + if ex { + serviceName = serviceNameValue.AsString() + } + + for i := 0; i < spanSlice.Len(); i++ { + otelSpan := spanSlice.At(i) + + instanaSpan, err := model.ConvertPDataSpanToInstanaSpan(fromS, otelSpan, serviceName, attributes) + if err != nil { + c.logger.Warn(fmt.Sprintf("Error converting Open Telemetry span to Instana span: %s", err.Error())) + continue + } + + spans = append(spans, instanaSpan) + } + + bundle.Spans = spans + + return bundle +} + +func (c *SpanConverter) Name() string { + return "SpanConverter" +} diff --git a/exporter/instanaexporter/internal/converter/span_converter_test.go b/exporter/instanaexporter/internal/converter/span_converter_test.go new file mode 100644 index 000000000000..66a1038ec641 --- /dev/null +++ b/exporter/instanaexporter/internal/converter/span_converter_test.go @@ -0,0 +1,266 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package converter + +import ( + "bytes" + "encoding/json" + "math/rand" + "testing" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + conventions "go.opentelemetry.io/collector/semconv/v1.8.0" + + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/backend" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/converter/model" +) + +type SpanOptions struct { + TraceID [16]byte + SpanID [8]byte + ParentID [8]byte + Error string + StartTimestamp time.Duration + EndTimestamp time.Duration +} + +func setupSpan(span *ptrace.Span, opts SpanOptions) { + var empty16 [16]byte + var empty8 [8]byte + + now := time.Now().UnixMilli() + + traceID := opts.TraceID + spanID := opts.SpanID + parentID := opts.ParentID + startTime := opts.StartTimestamp + endTime := opts.EndTimestamp + + if bytes.Equal(traceID[:], empty16[:]) { + traceID = generateTraceID() + } + + if bytes.Equal(spanID[:], empty8[:]) { + spanID = generateSpanID() + } + + if startTime == time.Second*0 { + startTime = time.Duration(now) + } + + if endTime == time.Second*0 { + endTime = startTime + 1000 + } + + if opts.Error != "" { + span.Status().SetCode(ptrace.StatusCodeError) + span.Status().SetMessage(opts.Error) + } + + if !bytes.Equal(parentID[:], empty8[:]) { + span.SetParentSpanID(pcommon.NewSpanID(parentID)) + } + + span.SetStartTimestamp(pcommon.Timestamp(startTime * 1e6)) + span.SetEndTimestamp(pcommon.Timestamp(endTime * 1e6)) + + span.SetSpanID(pcommon.NewSpanID(spanID)) + span.SetKind(ptrace.SpanKindClient) + span.SetName("my_operation") + span.SetTraceState(ptrace.TraceStateEmpty) + span.SetTraceID(pcommon.NewTraceID(traceID)) + + // adding attributes (tags in the instana side) + span.Attributes().Insert("some_key", pcommon.NewValueBool(true)) +} + +func generateAttrs() pcommon.Map { + rawmap := map[string]interface{}{ + "some_boolean_key": true, + "custom_attribute": "ok", + // test non empty pid + conventions.AttributeProcessPID: "1234", + // test non empty service name + conventions.AttributeServiceName: "myservice", + // test non empty instana host id + backend.AttributeInstanaHostID: "myhost1", + } + + attrs := pcommon.NewMapFromRaw(rawmap) + attrs.InsertBool("itistrue", true) + + return attrs +} + +func validateInstanaSpanBasics(sp model.Span, t *testing.T) { + if sp.SpanID == "" { + t.Error("expected span id not to be empty") + } + + if sp.TraceID == "" { + t.Error("expected trace id not to be empty") + } + + if sp.Name != "otel" { + t.Errorf("expected span name to be 'otel' but received '%v'", sp.Name) + } + + if sp.Timestamp <= 0 { + t.Errorf("expected timestamp to be provided but received %v", sp.Timestamp) + } + + if sp.Duration <= 0 { + t.Errorf("expected duration to be provided but received %v", sp.Duration) + } +} + +func validateBundle(jsonData []byte, t *testing.T, fn func(model.Span, *testing.T)) { + var bundle model.Bundle + + err := json.Unmarshal(jsonData, &bundle) + + if err != nil { + t.Fatal(err) + } + + if len(bundle.Spans) == 0 { + t.Log("bundle contains no spans") + return + } + + for _, span := range bundle.Spans { + fn(span, t) + } +} + +func validateSpanError(sp model.Span, shouldHaveError bool, t *testing.T) { + if shouldHaveError { + if sp.Ec <= 0 { + t.Error("expected span to have errors (ec = 1)") + } + + if sp.Data.Tags[model.InstanaDataError] == "" { + t.Error("expected data.error to exist") + } + + if sp.Data.Tags[model.InstanaDataErrorDetail] == "" { + t.Error("expected data.error_detail to exist") + } + + return + } + + if sp.Ec > 0 { + t.Error("expected span not to have errors (ec = 0)") + } + + if sp.Data.Tags[model.InstanaDataError] != "" { + t.Error("expected data.error to be empty") + } + + if sp.Data.Tags[model.InstanaDataErrorDetail] != "" { + t.Error("expected data.error_detail to be empty") + } +} + +func TestSpanBasics(t *testing.T) { + spanSlice := ptrace.NewSpanSlice() + + sp1 := spanSlice.AppendEmpty() + + setupSpan(&sp1, SpanOptions{}) + + attrs := generateAttrs() + conv := SpanConverter{} + bundle := conv.ConvertSpans(attrs, spanSlice) + data, _ := json.MarshalIndent(bundle, "", " ") + + validateBundle(data, t, func(sp model.Span, t *testing.T) { + validateInstanaSpanBasics(sp, t) + validateSpanError(sp, false, t) + }) +} + +func TestSpanCorrelation(t *testing.T) { + spanSlice := ptrace.NewSpanSlice() + + sp1 := spanSlice.AppendEmpty() + setupSpan(&sp1, SpanOptions{}) + + sp2 := spanSlice.AppendEmpty() + setupSpan(&sp2, SpanOptions{ + ParentID: sp1.SpanID().Bytes(), + }) + + sp3 := spanSlice.AppendEmpty() + setupSpan(&sp3, SpanOptions{ + ParentID: sp2.SpanID().Bytes(), + }) + + sp4 := spanSlice.AppendEmpty() + setupSpan(&sp4, SpanOptions{ + ParentID: sp1.SpanID().Bytes(), + }) + + attrs := generateAttrs() + conv := SpanConverter{} + bundle := conv.ConvertSpans(attrs, spanSlice) + data, _ := json.MarshalIndent(bundle, "", " ") + + spanIDList := make(map[string]bool) + + validateBundle(data, t, func(sp model.Span, t *testing.T) { + validateInstanaSpanBasics(sp, t) + validateSpanError(sp, false, t) + + spanIDList[sp.SpanID] = true + + if sp.ParentID != "" && !spanIDList[sp.ParentID] { + t.Errorf("span %v expected to have parent id %v", sp.SpanID, sp.ParentID) + } + }) +} +func TestSpanWithError(t *testing.T) { + spanSlice := ptrace.NewSpanSlice() + + sp1 := spanSlice.AppendEmpty() + setupSpan(&sp1, SpanOptions{ + Error: "some error", + }) + + attrs := generateAttrs() + conv := SpanConverter{} + bundle := conv.ConvertSpans(attrs, spanSlice) + data, _ := json.MarshalIndent(bundle, "", " ") + + validateBundle(data, t, func(sp model.Span, t *testing.T) { + validateInstanaSpanBasics(sp, t) + validateSpanError(sp, true, t) + }) +} + +func generateTraceID() (data [16]byte) { + rand.Read(data[:]) + + return data +} + +func generateSpanID() (data [8]byte) { + rand.Read(data[:]) + + return data +} diff --git a/exporter/instanaexporter/internal/otlptext/databuffer.go b/exporter/instanaexporter/internal/otlptext/databuffer.go new file mode 100644 index 000000000000..0039e2057dc6 --- /dev/null +++ b/exporter/instanaexporter/internal/otlptext/databuffer.go @@ -0,0 +1,328 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file is copied from "go.opentelemetry.io/collector/exporter/loggingexporter/internal/otlptext" +// It should be kept uptodate with the original version +// TODO: Refactor to remove the duplication + +package otlptext // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/otlptext" + +import ( + "bytes" + "fmt" + "math" + "strconv" + "strings" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +type dataBuffer struct { + buf bytes.Buffer +} + +func (b *dataBuffer) logEntry(format string, a ...interface{}) { + b.buf.WriteString(fmt.Sprintf(format, a...)) + b.buf.WriteString("\n") +} + +func (b *dataBuffer) logAttr(attr string, value string) { + b.logEntry(" %-15s: %s", attr, value) +} + +func (b *dataBuffer) logAttributes(attr string, m pcommon.Map) { + if m.Len() == 0 { + return + } + + b.logEntry("%s:", attr) + m.Range(func(k string, v pcommon.Value) bool { + b.logEntry(" -> %s: %s(%s)", k, v.Type().String(), attributeValueToString(v)) + return true + }) +} + +func (b *dataBuffer) logInstrumentationScope(il pcommon.InstrumentationScope) { + b.logEntry( + "InstrumentationScope %s %s", + il.Name(), + il.Version()) + b.logAttributes("InstrumentationScope attributes", il.Attributes()) +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logMetricDescriptor(md pmetric.Metric) { + b.logEntry("Descriptor:") + b.logEntry(" -> Name: %s", md.Name()) + b.logEntry(" -> Description: %s", md.Description()) + b.logEntry(" -> Unit: %s", md.Unit()) + b.logEntry(" -> DataType: %s", md.DataType().String()) +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logMetricDataPoints(m pmetric.Metric) { + switch m.DataType() { + case pmetric.MetricDataTypeNone: + return + case pmetric.MetricDataTypeGauge: + b.logNumberDataPoints(m.Gauge().DataPoints()) + case pmetric.MetricDataTypeSum: + data := m.Sum() + b.logEntry(" -> IsMonotonic: %t", data.IsMonotonic()) + b.logEntry(" -> AggregationTemporality: %s", data.AggregationTemporality().String()) + b.logNumberDataPoints(data.DataPoints()) + case pmetric.MetricDataTypeHistogram: + data := m.Histogram() + b.logEntry(" -> AggregationTemporality: %s", data.AggregationTemporality().String()) + b.logHistogramDataPoints(data.DataPoints()) + case pmetric.MetricDataTypeExponentialHistogram: + data := m.ExponentialHistogram() + b.logEntry(" -> AggregationTemporality: %s", data.AggregationTemporality().String()) + b.logExponentialHistogramDataPoints(data.DataPoints()) + case pmetric.MetricDataTypeSummary: + data := m.Summary() + b.logDoubleSummaryDataPoints(data.DataPoints()) + } +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logNumberDataPoints(ps pmetric.NumberDataPointSlice) { + for i := 0; i < ps.Len(); i++ { + p := ps.At(i) + b.logEntry("NumberDataPoints #%d", i) + b.logDataPointAttributes(p.Attributes()) + + b.logEntry("StartTimestamp: %s", p.StartTimestamp()) + b.logEntry("Timestamp: %s", p.Timestamp()) + switch p.ValueType() { + case pmetric.NumberDataPointValueTypeInt: + b.logEntry("Value: %d", p.IntVal()) + case pmetric.NumberDataPointValueTypeDouble: + b.logEntry("Value: %f", p.DoubleVal()) + } + } +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logHistogramDataPoints(ps pmetric.HistogramDataPointSlice) { + for i := 0; i < ps.Len(); i++ { + p := ps.At(i) + b.logEntry("HistogramDataPoints #%d", i) + b.logDataPointAttributes(p.Attributes()) + + b.logEntry("StartTimestamp: %s", p.StartTimestamp()) + b.logEntry("Timestamp: %s", p.Timestamp()) + b.logEntry("Count: %d", p.Count()) + + if p.HasSum() { + b.logEntry("Sum: %f", p.Sum()) + } + + if p.HasMin() { + b.logEntry("Min: %f", p.Min()) + } + + if p.HasMax() { + b.logEntry("Max: %f", p.Max()) + } + + for i := 0; i < p.ExplicitBounds().Len(); i++ { + b.logEntry("ExplicitBounds #%d: %f", i, p.ExplicitBounds().At(i)) + } + + for j := 0; j < p.BucketCounts().Len(); j++ { + b.logEntry("Buckets #%d, Count: %d", j, p.BucketCounts().At(j)) + } + } +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logExponentialHistogramDataPoints(ps pmetric.ExponentialHistogramDataPointSlice) { + for i := 0; i < ps.Len(); i++ { + p := ps.At(i) + b.logEntry("ExponentialHistogramDataPoints #%d", i) + b.logDataPointAttributes(p.Attributes()) + + b.logEntry("StartTimestamp: %s", p.StartTimestamp()) + b.logEntry("Timestamp: %s", p.Timestamp()) + b.logEntry("Count: %d", p.Count()) + + if p.HasSum() { + b.logEntry("Sum: %f", p.Sum()) + } + + if p.HasMin() { + b.logEntry("Min: %f", p.Min()) + } + + if p.HasMax() { + b.logEntry("Max: %f", p.Max()) + } + + scale := int(p.Scale()) + factor := math.Ldexp(math.Ln2, -scale) + // Note: the equation used here, which is + // math.Exp(index * factor) + // reports +Inf as the _lower_ boundary of the bucket nearest + // infinity, which is incorrect and can be addressed in various + // ways. The OTel-Go implementation of this histogram pending + // in https://github.com/open-telemetry/opentelemetry-go/pull/2393 + // uses a lookup table for the last finite boundary, which can be + // easily computed using `math/big` (for scales up to 20). + + negB := p.Negative().BucketCounts() + posB := p.Positive().BucketCounts() + + for i := 0; i < negB.Len(); i++ { + pos := negB.Len() - i - 1 + index := p.Negative().Offset() + int32(pos) + lower := math.Exp(float64(index) * factor) + upper := math.Exp(float64(index+1) * factor) + b.logEntry("Bucket (%f, %f], Count: %d", -upper, -lower, negB.At(pos)) + } + + if p.ZeroCount() != 0 { + b.logEntry("Bucket [0, 0], Count: %d", p.ZeroCount()) + } + + for pos := 0; pos < posB.Len(); pos++ { + index := p.Positive().Offset() + int32(pos) + lower := math.Exp(float64(index) * factor) + upper := math.Exp(float64(index+1) * factor) + b.logEntry("Bucket [%f, %f), Count: %d", lower, upper, posB.At(pos)) + } + } +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logDoubleSummaryDataPoints(ps pmetric.SummaryDataPointSlice) { + for i := 0; i < ps.Len(); i++ { + p := ps.At(i) + b.logEntry("SummaryDataPoints #%d", i) + b.logDataPointAttributes(p.Attributes()) + + b.logEntry("StartTimestamp: %s", p.StartTimestamp()) + b.logEntry("Timestamp: %s", p.Timestamp()) + b.logEntry("Count: %d", p.Count()) + b.logEntry("Sum: %f", p.Sum()) + + quantiles := p.QuantileValues() + for i := 0; i < quantiles.Len(); i++ { + quantile := quantiles.At(i) + b.logEntry("QuantileValue #%d: Quantile %f, Value %f", i, quantile.Quantile(), quantile.Value()) + } + } +} + +//lint:ignore U1000 Ignore unused function temporarily until metrics added +func (b *dataBuffer) logDataPointAttributes(attributes pcommon.Map) { + b.logAttributes("Data point attributes", attributes) +} + +func (b *dataBuffer) logEvents(description string, se ptrace.SpanEventSlice) { + if se.Len() == 0 { + return + } + + b.logEntry("%s:", description) + for i := 0; i < se.Len(); i++ { + e := se.At(i) + b.logEntry("SpanEvent #%d", i) + b.logEntry(" -> Name: %s", e.Name()) + b.logEntry(" -> Timestamp: %s", e.Timestamp()) + b.logEntry(" -> DroppedAttributesCount: %d", e.DroppedAttributesCount()) + + if e.Attributes().Len() == 0 { + continue + } + b.logEntry(" -> Attributes:") + e.Attributes().Range(func(k string, v pcommon.Value) bool { + b.logEntry(" -> %s: %s(%s)", k, v.Type().String(), attributeValueToString(v)) + return true + }) + } +} + +func (b *dataBuffer) logLinks(description string, sl ptrace.SpanLinkSlice) { + if sl.Len() == 0 { + return + } + + b.logEntry("%s:", description) + + for i := 0; i < sl.Len(); i++ { + l := sl.At(i) + b.logEntry("SpanLink #%d", i) + b.logEntry(" -> Trace ID: %s", l.TraceID().HexString()) + b.logEntry(" -> ID: %s", l.SpanID().HexString()) + b.logEntry(" -> TraceState: %s", l.TraceState()) + b.logEntry(" -> DroppedAttributesCount: %d", l.DroppedAttributesCount()) + if l.Attributes().Len() == 0 { + continue + } + b.logEntry(" -> Attributes:") + l.Attributes().Range(func(k string, v pcommon.Value) bool { + b.logEntry(" -> %s: %s(%s)", k, v.Type().String(), attributeValueToString(v)) + return true + }) + } +} + +func attributeValueToString(v pcommon.Value) string { + switch v.Type() { + case pcommon.ValueTypeString: + return v.StringVal() + case pcommon.ValueTypeBool: + return strconv.FormatBool(v.BoolVal()) + case pcommon.ValueTypeDouble: + return strconv.FormatFloat(v.DoubleVal(), 'f', -1, 64) + case pcommon.ValueTypeInt: + return strconv.FormatInt(v.IntVal(), 10) + case pcommon.ValueTypeSlice: + return sliceToString(v.SliceVal()) + case pcommon.ValueTypeMap: + return mapToString(v.MapVal()) + default: + return fmt.Sprintf("", v.Type()) + } +} + +func sliceToString(s pcommon.Slice) string { + var b strings.Builder + b.WriteByte('[') + for i := 0; i < s.Len(); i++ { + if i < s.Len()-1 { + fmt.Fprintf(&b, "%s, ", attributeValueToString(s.At(i))) + } else { + b.WriteString(attributeValueToString(s.At(i))) + } + } + + b.WriteByte(']') + return b.String() +} + +func mapToString(m pcommon.Map) string { + var b strings.Builder + b.WriteString("{\n") + + m.Sort().Range(func(k string, v pcommon.Value) bool { + fmt.Fprintf(&b, " -> %s: %s(%s)\n", k, v.Type(), v.AsString()) + return true + }) + b.WriteByte('}') + return b.String() +} diff --git a/exporter/instanaexporter/internal/otlptext/traces.go b/exporter/instanaexporter/internal/otlptext/traces.go new file mode 100644 index 000000000000..fe1a23d870db --- /dev/null +++ b/exporter/instanaexporter/internal/otlptext/traces.go @@ -0,0 +1,63 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlptext // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/otlptext" + +import "go.opentelemetry.io/collector/pdata/ptrace" + +// NewTextTracesMarshaler returns a serializer.TracesMarshaler to encode to OTLP text bytes. +func NewTextTracesMarshaler() ptrace.Marshaler { + return textTracesMarshaler{} +} + +type textTracesMarshaler struct{} + +// MarshalTraces pdata.Traces to OTLP text. +func (textTracesMarshaler) MarshalTraces(td ptrace.Traces) ([]byte, error) { + buf := dataBuffer{} + rss := td.ResourceSpans() + for i := 0; i < rss.Len(); i++ { + buf.logEntry("ResourceSpans #%d", i) + rs := rss.At(i) + buf.logAttributes("Resource labels", rs.Resource().Attributes()) + ilss := rs.ScopeSpans() + for j := 0; j < ilss.Len(); j++ { + buf.logEntry("InstrumentationLibrarySpans #%d", j) + ils := ilss.At(j) + buf.logInstrumentationScope(ils.Scope()) + + spans := ils.Spans() + for k := 0; k < spans.Len(); k++ { + buf.logEntry("Span #%d", k) + span := spans.At(k) + buf.logAttr("Trace ID", span.TraceID().HexString()) + buf.logAttr("Parent ID", span.ParentSpanID().HexString()) + buf.logAttr("ID", span.SpanID().HexString()) + buf.logAttr("Name", span.Name()) + buf.logAttr("Kind", span.Kind().String()) + buf.logAttr("Start time", span.StartTimestamp().String()) + buf.logAttr("End time", span.EndTimestamp().String()) + + buf.logAttr("Status code", span.Status().Code().String()) + buf.logAttr("Status message", span.Status().Message()) + + buf.logAttributes("Attributes", span.Attributes()) + buf.logEvents("Events", span.Events()) + buf.logLinks("Links", span.Links()) + } + } + } + + return buf.buf.Bytes(), nil +} diff --git a/exporter/instanaexporter/internal/testutils/test_utils.go b/exporter/instanaexporter/internal/testutils/test_utils.go new file mode 100644 index 000000000000..cc1bc480b962 --- /dev/null +++ b/exporter/instanaexporter/internal/testutils/test_utils.go @@ -0,0 +1,43 @@ +// Copyright 2022, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/instanaexporter/internal/testutils" + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +var ( + testAttributes = map[string]string{"instana.agent": "agent1"} + // TestTraces traces for tests. + TestTraces = newTracesWithAttributeMap(testAttributes) +) + +func fillAttributeMap(attrs pcommon.Map, mp map[string]string) { + attrs.Clear() + attrs.EnsureCapacity(len(mp)) + for k, v := range mp { + attrs.Insert(k, pcommon.NewValueString(v)) + } +} + +func newTracesWithAttributeMap(mp map[string]string) ptrace.Traces { + traces := ptrace.NewTraces() + resourceSpans := traces.ResourceSpans() + rs := resourceSpans.AppendEmpty() + fillAttributeMap(rs.Resource().Attributes(), mp) + rs.ScopeSpans().AppendEmpty().Spans().AppendEmpty() + return traces +} diff --git a/unreleased/feat_add-instana-exp-impl.yaml b/unreleased/feat_add-instana-exp-impl.yaml new file mode 100755 index 000000000000..02d173cf7424 --- /dev/null +++ b/unreleased/feat_add-instana-exp-impl.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: new_component + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: instanaexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add Instana exporter implementation + +# One or more tracking issues related to the change +issues: [13395] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: