Skip to content

Commit

Permalink
[exporter/instana] Add implementation (#13620)
Browse files Browse the repository at this point in the history
* Add Instana exporter implementation

Signed-off-by: Martin Hickey <[email protected]>

* Add unit tests

Signed-off-by: Martin Hickey <[email protected]>

* Add changelog

Signed-off-by: Martin Hickey <[email protected]>

* Add more unit tests

Signed-off-by: Martin Hickey <[email protected]>

* Update fater review

Review comments:
- #13620 (review)

Signed-off-by: Martin Hickey <[email protected]>

* Update after review

Review comments:

- #13620 (comment)
- #13620 (comment)
- #13620 (comment)
- #13620 (comment)
- #13620 (comment)
- #13620 (comment)
- #13620 (comment)
- #13620 (comment)
- #13620 (comment)

Signed-off-by: Martin Hickey <[email protected]>

Signed-off-by: Martin Hickey <[email protected]>
  • Loading branch information
hickeyma authored Aug 30, 2022
1 parent d67fbce commit a978450
Show file tree
Hide file tree
Showing 20 changed files with 1,469 additions and 17 deletions.
5 changes: 5 additions & 0 deletions exporter/instanaexporter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package instanaexporter // import "github.com/open-telemetry/opentelemetry-colle

import (
"errors"
"net/url"
"strings"

"go.opentelemetry.io/collector/config"
Expand Down Expand Up @@ -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
}
12 changes: 10 additions & 2 deletions exporter/instanaexporter/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
137 changes: 137 additions & 0 deletions exporter/instanaexporter/exporter.go
Original file line number Diff line number Diff line change
@@ -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
}
95 changes: 95 additions & 0 deletions exporter/instanaexporter/exporter_test.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 4 additions & 13 deletions exporter/instanaexporter/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
Expand Down
5 changes: 3 additions & 2 deletions exporter/instanaexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions exporter/instanaexporter/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions exporter/instanaexporter/internal/backend/config.go
Original file line number Diff line number Diff line change
@@ -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"
)
Loading

0 comments on commit a978450

Please sign in to comment.