Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[exporter/mezmoexporter] Add the Mezmo Log exporter #9743

Merged
merged 40 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a5a0553
Added implementation of Mezmo Log exporter.
billmeyer Apr 21, 2022
a8d278e
Additional files to prepare for PR to contrib
billmeyer Apr 25, 2022
6f84046
Initial revision
billmeyer May 2, 2022
ca53986
Changes to support batching of log uploads to not exceed maximum allo…
billmeyer May 2, 2022
cb33fa1
Update exporter/mezmoexporter/exporter.go
billmeyer May 6, 2022
2437732
Update exporter/mezmoexporter/exporter.go
billmeyer May 6, 2022
2e97954
Update exporter/mezmoexporter/exporter.go
billmeyer May 6, 2022
298b043
Update exporter/mezmoexporter/exporter.go
billmeyer May 6, 2022
97ab240
Update exporter/mezmoexporter/exporter.go
billmeyer May 6, 2022
20473d4
Update exporter/mezmoexporter/exporter.go
billmeyer May 6, 2022
43298f0
Added the appropriate code owners the the Mezmo Exporter
billmeyer May 6, 2022
356f79a
Moved test functions to test file
billmeyer May 6, 2022
257979e
Cleaned up README and standardized URLs across examples
billmeyer May 6, 2022
c6a2af1
Cleanup to remove commented code and non-exported functions
billmeyer May 6, 2022
067e03a
Merge branch 'mezmo-exporter' of https://github.com/billmeyer/opentel…
billmeyer May 6, 2022
583ec71
Fixed a typo
billmeyer May 6, 2022
670c7b4
Added TODO entry for removing hostname from the HTTP POST when it's n…
billmeyer May 6, 2022
0c8fa40
Removed year from the Copyright header
billmeyer May 10, 2022
2b762a3
Fixed a typo in the mezmo exporter definition
billmeyer May 10, 2022
6000ce6
Made the ingest_url an optional configuration with a known default va…
billmeyer May 10, 2022
6158c61
Changes to enable a default ingest URL as well as allow an override f…
billmeyer May 10, 2022
8a27a6f
Updates from rm -fr go.sum
billmeyer May 10, 2022
3d7d190
Merge branch 'main' into mezmo-exporter
billmeyer May 10, 2022
e1a7fe9
Added new Mezmo Exporter component
billmeyer May 11, 2022
634744d
Merge branch 'mezmo-exporter' of https://github.com/billmeyer/opentel…
billmeyer May 11, 2022
a938fb5
Added return value checks for some tests
billmeyer May 11, 2022
e6dae93
Removed some unneeded code from the last merge
billmeyer May 11, 2022
214d563
Reverted the release version to unreleased
billmeyer May 11, 2022
69cb016
Proper version for the new component
billmeyer May 11, 2022
2599e98
Applied make goporto updates
billmeyer May 11, 2022
9af6b95
Fixed a unit test case for the default config test
billmeyer May 11, 2022
c4bf367
Merge branch 'main' into mezmo-exporter
billmeyer May 12, 2022
e7c6377
Bumped the version number for the go.opentelemetry.io/collector versions
billmeyer May 12, 2022
6439b19
Module updates for latest release
billmeyer May 12, 2022
e29d446
Removed the newrelicexporter as it has been removed
billmeyer May 12, 2022
c083234
Merge branch 'main' into mezmo-exporter
billmeyer May 13, 2022
6af0966
Merge branch 'main' into mezmo-exporter
billmeyer May 14, 2022
19c90e9
Updates from the latest make gotidy for the merge to main
billmeyer May 16, 2022
91b596c
Pulled semconv from go.opentelemetry.io/collector/semconv instead of …
billmeyer May 16, 2022
ffd54c5
Moved the mezmo new component under Unreleased and fixed the associat…
billmeyer May 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ exporter/kafkaexporter/ @open-telemetry/collector-c
exporter/loadbalancingexporter/ @open-telemetry/collector-contrib-approvers @jpkrohling
exporter/logzioexporter/ @open-telemetry/collector-contrib-approvers @jkowall @Doron-Bargo @yotamloe
exporter/lokiexporter/ @open-telemetry/collector-contrib-approvers @gramidt @jpkrohling
exporter/mezmoexporter/ @open-telemetry/collector-contrib-approvers @dashpole @billmeyer @gjanco
exporter/newrelicexporter/ @open-telemetry/collector-contrib-approvers @alanwest @jack-berg @nrcventura
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
exporter/observiqexporter/ @open-telemetry/collector-contrib-approvers @binaryfissiongames
exporter/prometheusexporter/ @open-telemetry/collector-contrib-approvers @Aneurysm9
Expand Down
1 change: 1 addition & 0 deletions exporter/mezmoexporter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
30 changes: 30 additions & 0 deletions exporter/mezmoexporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Mezmo Exporter

This exporter supports sending OpenTelemetry log data to [LogDNA (Mezmo)](https://logdna.com).

# Configuration options:

- `ingest_url` (required): Use the default URL of `https://logs.logdna.com/log/ingest` unless a custom URL has been provided for your account.
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
- `ingest_key` (required): Ingestion key used to send log data to LogDNA. See [Ingestion Keys](https://docs.logdna.com/docs/ingestion-key) for more details.

# Example:
## Simple Log Data

```yaml
receivers:
otlp:
protocols:
grpc:
endpoint: ":4317"

exporters:
mezmo:
ingest_url = "https://logs.logdna.com/log/ingest"
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
ingest_key = "00000000000000000000000000000000"

service:
pipelines:
logs:
receivers: [ otlp ]
exporters: [ mezmo ]
```
79 changes: 79 additions & 0 deletions exporter/mezmoexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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.

package mezmoexporter

import (
"fmt"
"net/url"
"time"

"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/exporter/exporterhelper"
)

const (
// defaultTimeout
defaultTimeout time.Duration = 5 * time.Second

// defaultIngestURL
defaultIngestURL = "https://logs.logdna.com/log/ingest"

// See https://docs.logdna.com/docs/ingestion#service-limits for details

// Maximum payload in bytes that can be POST'd to the REST endpoint
maxBodySize = 10 * 1024 * 1024
maxMessageSize = 16 * 1024
maxMetaDataSize = 32 * 1024
maxAppnameLen = 512
maxLogLevelLen = 80
)

// Config defines configuration for Mezmo exporter.
type Config struct {
config.ExporterSettings `mapstructure:",squash"`
confighttp.HTTPClientSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct.
exporterhelper.QueueSettings `mapstructure:"sending_queue"`
exporterhelper.RetrySettings `mapstructure:"retry_on_failure"`

// IngestURL is the URL to send telemetry to.
IngestURL string `mapstructure:"ingest_url"`
billmeyer marked this conversation as resolved.
Show resolved Hide resolved

// Token is the authentication token provided by Mezmo.
IngestKey string `mapstructure:"ingest_key"`
}

// returns default http client settings
func createDefaultHTTPClientSettings() confighttp.HTTPClientSettings {
return confighttp.HTTPClientSettings{
Timeout: defaultTimeout,
}
}

func (c *Config) Validate() error {
dashpole marked this conversation as resolved.
Show resolved Hide resolved
var err error
var parsed *url.URL

parsed, err = url.Parse(c.IngestURL)
if c.IngestURL == "" || err != nil {
return fmt.Errorf("\"ingest_url\" must be a valid URL")
}

if parsed.Host == "" {
return fmt.Errorf("\"ingest_url\" must contain a valid host")
}

return nil
}
47 changes: 47 additions & 0 deletions exporter/mezmoexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.

package mezmoexporter

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/service/servicetest"
)

func TestLoadConfig(t *testing.T) {
factories, err := componenttest.NopFactories()
assert.Nil(t, err)

factory := NewFactory()
factories.Exporters[typeStr] = factory
cfg, err := servicetest.LoadConfigAndValidate(filepath.Join("testdata", "config.yaml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

loadedCfg := cfg.Exporters[config.NewComponentID(typeStr)]

// IngestURL doesn't have a default value so set it directly.
expectedCfg := factory.CreateDefaultConfig().(*Config)
expectedCfg.IngestURL = defaultIngestURL
expectedCfg.IngestKey = "00000000000000000000000000000000"

assert.Equal(t, expectedCfg, loadedCfg)
}
16 changes: 16 additions & 0 deletions exporter/mezmoexporter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2020, OpenTelemetry Authors
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
//
// 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 mezmoexporter implements an exporter that sends data to Mezmo.
package mezmoexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/mezmoexporter"
17 changes: 17 additions & 0 deletions exporter/mezmoexporter/example/otel-collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
receivers:
nop:

processors:
nop:

exporters:
mezmo:
ingest_url: "https://logs.logdna.com/log/ingest"
ingest_key: "00000000000000000000000000000000"

service:
pipelines:
logs:
receivers: [ nop ]
processors: [ nop ]
exporters: [ mezmo ]
166 changes: 166 additions & 0 deletions exporter/mezmoexporter/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// 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.

package mezmoexporter

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/plog"
)

type mezmoExporter struct {
config *Config
settings component.TelemetrySettings
client *http.Client
wg sync.WaitGroup
}

type MezmoLogLine struct {
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
Timestamp int64 `json:"timestamp"`
Line string `json:"line"`
App string `json:"app"`
Level string `json:"level"`
Meta map[string]string `json:"meta"`
}

type MezmoLogBody struct {
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
Lines []MezmoLogLine `json:"lines"`
}

func newLogsExporter(config *Config, settings component.TelemetrySettings) *mezmoExporter {
var e = &mezmoExporter{
config: config,
settings: settings,
}
return e
}

func (m *mezmoExporter) pushLogData(ctx context.Context, ld plog.Logs) error {
m.wg.Add(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this wg is neccessary. stop() just needs to terminate background goroutines.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needs to be resolved.

defer m.wg.Done()

return m.logDataToMezmo(ld)
}

func (m *mezmoExporter) start(_ context.Context, host component.Host) (err error) {
m.client, err = m.config.HTTPClientSettings.ToClient(host.GetExtensions(), m.settings)
return err
}

func (m *mezmoExporter) stop(context.Context) (err error) {
m.wg.Wait()
return nil
}

func (m *mezmoExporter) logDataToMezmo(ld plog.Logs) error {
var errs error

var lines []MezmoLogLine

// Convert the log resources to mezmo lines...
resourceLogs := ld.ResourceLogs()
for i := 0; i < resourceLogs.Len(); i++ {
ills := resourceLogs.At(i).ScopeLogs()
for j := 0; j < ills.Len(); j++ {
logs := ills.At(j).LogRecords()

for k := 0; k < logs.Len(); k++ {
log := logs.At(k)

// Convert Attributes to meta fields being mindful of the maxMetaDataSize restriction
attrs := map[string]string{}
attrs["trace.id"] = log.TraceID().HexString()
billmeyer marked this conversation as resolved.
Show resolved Hide resolved
attrs["span.id"] = log.SpanID().HexString()
log.Attributes().Range(func(k string, v pcommon.Value) bool {
attrs[k] = truncateString(v.StringVal(), maxMetaDataSize)
return true
})

s, _ := log.Attributes().Get("appname")
app := s.StringVal()

line := MezmoLogLine{
Timestamp: log.Timestamp().AsTime().UTC().UnixMilli(),
Line: truncateString(log.Body().StringVal(), maxMessageSize),
App: truncateString(app, maxAppnameLen),
Level: truncateString(log.SeverityText(), maxLogLevelLen),
Meta: attrs,
}
lines = append(lines, line)
}
}
}

// Send them to Mezmo in batches < 10MB in size
var b strings.Builder
b.WriteString("{\"lines\": [")
dashpole marked this conversation as resolved.
Show resolved Hide resolved

var lineBytes []byte
for i, line := range lines {
if lineBytes, errs = json.Marshal(line); errs != nil {
return fmt.Errorf("error Creating JSON payload: %s", errs)
}

var newBufSize = b.Len() + len(lineBytes)
if newBufSize >= maxBodySize-2 {
str := b.String()
str = str[:len(str)-1] + "]}"
if errs = m.sendLinesToMezmo(str); errs != nil {
return errs
}
b.Reset()
b.WriteString("{\"lines\": [")
}

b.Write(lineBytes)

if i < len(lines)-1 {
b.WriteRune(',')
}
}

str := b.String() + "]}"
if errs = m.sendLinesToMezmo(str); errs != nil {
return errs
}

return nil
}

func (m *mezmoExporter) sendLinesToMezmo(post string) (errs error) {
var hostname = "otel"
billmeyer marked this conversation as resolved.
Show resolved Hide resolved

url := fmt.Sprintf("%s?hostname=%s", m.config.IngestURL, hostname)

req, _ := http.NewRequest("POST", url, bytes.NewBuffer([]byte(post)))
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("apikey", m.config.IngestKey)

var res *http.Response
if res, errs = http.DefaultClient.Do(req); errs != nil {
return fmt.Errorf("failed to POST log to Mezmo: %s", errs)
}

return res.Body.Close()
}
Loading