Skip to content

Commit 3136cfc

Browse files
authored
Merge pull request #1 from braydonk/gcloud_logs_exporter
exporter/googlecloudloggingexporter: did it
2 parents a72a649 + fe472ef commit 3136cfc

File tree

7 files changed

+1017
-0
lines changed

7 files changed

+1017
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include ../../Makefile.Common
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package googlecloudloggingexporter
2+
3+
import (
4+
"go.opentelemetry.io/collector/config"
5+
"go.opentelemetry.io/collector/exporter/exporterhelper"
6+
"go.uber.org/zap"
7+
)
8+
9+
type Config struct {
10+
config.ExporterSettings `mapstructure:",squash"`
11+
ProjectID string `mapstructure:"project"`
12+
UserAgent string `mapstructure:"user_agent"`
13+
LogName string `mapstructure:"log_name"`
14+
15+
Endpoint string `mapstructure:"endpoint"`
16+
// Only has effect if Endpoint is not ""
17+
UseInsecure bool `mapstructure:"use_insecure"`
18+
19+
// Timeout for all API calls. If not set, defaults to 12 seconds.
20+
exporterhelper.TimeoutSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct.
21+
exporterhelper.QueueSettings `mapstructure:"sending_queue"`
22+
exporterhelper.RetrySettings `mapstructure:"retry_on_failure"`
23+
24+
logger *zap.Logger
25+
}
26+
27+
func (c Config) Validate() error {
28+
return nil
29+
}
30+
31+
func (config *Config) enforcedQueueSettings() exporterhelper.QueueSettings {
32+
return exporterhelper.QueueSettings{
33+
Enabled: true,
34+
// due to the sequence token, there can be only one request in flight
35+
NumConsumers: 1,
36+
QueueSize: config.QueueSettings.QueueSize,
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package googlecloudloggingexporter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"cloud.google.com/go/logging"
8+
"github.com/google/uuid"
9+
"go.opentelemetry.io/collector/component"
10+
"go.opentelemetry.io/collector/consumer"
11+
"go.opentelemetry.io/collector/exporter/exporterhelper"
12+
"go.opentelemetry.io/collector/model/pdata"
13+
"go.uber.org/zap"
14+
)
15+
16+
type exporter struct {
17+
Config *Config
18+
logger *zap.Logger
19+
collectorID string
20+
cloudLogger *logging.Logger
21+
}
22+
23+
func newCloudLoggingExporter(config *Config, params component.ExporterCreateSettings) (component.LogsExporter, error) {
24+
loggingExporter, err := newCloudLoggingExporter(config, params)
25+
if err != nil {
26+
return nil, err
27+
}
28+
return exporterhelper.NewLogsExporter(
29+
config,
30+
params,
31+
loggingExporter.ConsumeLogs,
32+
exporterhelper.WithQueue(config.enforcedQueueSettings()),
33+
exporterhelper.WithRetry(config.RetrySettings))
34+
}
35+
36+
func newCloudLoggingLogExporter(config *Config, params component.ExporterCreateSettings) (component.LogsExporter, error) {
37+
// Validate the passed config.
38+
if err := config.Validate(); err != nil {
39+
return nil, err
40+
}
41+
42+
// Generate a Collector ID.
43+
collectorIdentifier, err := uuid.NewRandom()
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
// Read project ID from Metadata if not specified by config.
49+
if config.ProjectID == "" {
50+
projectId, err := readProjectIdMetadata()
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to read Google Cloud project ID: %v", err)
53+
}
54+
config.ProjectID = projectId
55+
}
56+
57+
// Create Cloud Logging logger with project ID.
58+
client, err := logging.NewClient(context.Background(), config.ProjectID)
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to create Google Cloud Logging client: %v", err)
61+
}
62+
defer client.Close()
63+
logger := client.Logger(config.LogName)
64+
65+
// Create the logging exporter.
66+
loggingExporter := &exporter{
67+
Config: config,
68+
logger: params.Logger,
69+
collectorID: collectorIdentifier.String(),
70+
cloudLogger: logger,
71+
}
72+
return loggingExporter, nil
73+
}
74+
75+
func (e *exporter) ConsumeLogs(ctx context.Context, ld pdata.Logs) error {
76+
logEntries, dropped := logsToEntries(e.Config, e.logger, ld)
77+
if len(logEntries) == 0 {
78+
return nil
79+
}
80+
if dropped > 0 {
81+
e.logger.Debug("Dropped logs", zap.Any("logsDropped", dropped))
82+
}
83+
84+
for _, logEntry := range logEntries {
85+
e.logger.Debug("Adding log entry", zap.Any("entry", logEntry))
86+
e.cloudLogger.Log(logEntry)
87+
}
88+
e.logger.Debug("Log entries successfully buffered")
89+
err := e.cloudLogger.Flush()
90+
if err != nil {
91+
e.logger.Error("error force flushing logs. Skipping to next logPusher.", zap.Error(err))
92+
}
93+
return nil
94+
}
95+
96+
func (e *exporter) Capabilities() consumer.Capabilities {
97+
return consumer.Capabilities{MutatesData: false}
98+
}
99+
100+
func (e *exporter) Shutdown(ctx context.Context) error {
101+
// Flush the remaining logs before shutting down the exporter.
102+
if e.cloudLogger != nil {
103+
err := e.cloudLogger.Flush()
104+
if err != nil {
105+
return err
106+
}
107+
}
108+
return nil
109+
}
110+
111+
func (e *exporter) Start(ctx context.Context, host component.Host) error {
112+
return nil
113+
}
114+
115+
func logsToEntries(config *Config, logger *zap.Logger, ld pdata.Logs) ([]logging.Entry, int) {
116+
entries := []logging.Entry{}
117+
dropped := 0
118+
rls := ld.ResourceLogs()
119+
for i := 0; i < rls.Len(); i++ {
120+
rl := rls.At(i)
121+
resourceAttrs := attrsValue(rl.Resource().Attributes())
122+
ills := rl.InstrumentationLibraryLogs()
123+
for j := 0; j < ills.Len(); j++ {
124+
ils := ills.At(j)
125+
logs := ils.LogRecords()
126+
for k := 0; k < logs.Len(); k++ {
127+
log := logs.At(k)
128+
entry, err := logToEntry(config, resourceAttrs, log)
129+
if err != nil {
130+
logger.Debug("Failed to convert to Cloud Logging Entry", zap.Error(err))
131+
dropped++
132+
} else {
133+
entries = append(entries, entry)
134+
}
135+
}
136+
}
137+
}
138+
return entries, dropped
139+
}
140+
141+
type entryPayload struct {
142+
Message string `json:"message"`
143+
}
144+
145+
func logToEntry(config *Config, attributes map[string]interface{}, log pdata.LogRecord) (logging.Entry, error) {
146+
payload := entryPayload{
147+
Message: log.Body().AsString(),
148+
}
149+
return logging.Entry{
150+
Payload: payload,
151+
Timestamp: log.Timestamp().AsTime(),
152+
Severity: logging.Severity(log.SeverityNumber()),
153+
LogName: config.LogName,
154+
Trace: log.TraceID().HexString(),
155+
SpanID: log.SpanID().HexString(),
156+
}, nil
157+
}
158+
159+
func attrsValue(attrs pdata.AttributeMap) map[string]interface{} {
160+
if attrs.Len() == 0 {
161+
return nil
162+
}
163+
out := make(map[string]interface{}, attrs.Len())
164+
attrs.Range(func(k string, v pdata.AttributeValue) bool {
165+
out[k] = attrValue(v)
166+
return true
167+
})
168+
return out
169+
}
170+
171+
func attrValue(value pdata.AttributeValue) interface{} {
172+
switch value.Type() {
173+
case pdata.AttributeValueTypeInt:
174+
return value.IntVal()
175+
case pdata.AttributeValueTypeBool:
176+
return value.BoolVal()
177+
case pdata.AttributeValueTypeDouble:
178+
return value.DoubleVal()
179+
case pdata.AttributeValueTypeString:
180+
return value.StringVal()
181+
case pdata.AttributeValueTypeMap:
182+
values := map[string]interface{}{}
183+
value.MapVal().Range(func(k string, v pdata.AttributeValue) bool {
184+
values[k] = attrValue(v)
185+
return true
186+
})
187+
return values
188+
case pdata.AttributeValueTypeArray:
189+
arrayVal := value.SliceVal()
190+
values := make([]interface{}, arrayVal.Len())
191+
for i := 0; i < arrayVal.Len(); i++ {
192+
values[i] = attrValue(arrayVal.At(i))
193+
}
194+
return values
195+
case pdata.AttributeValueTypeEmpty:
196+
return nil
197+
default:
198+
return nil
199+
}
200+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package googlecloudloggingexporter
2+
3+
import (
4+
"context"
5+
"errors"
6+
"time"
7+
8+
"go.opentelemetry.io/collector/component"
9+
"go.opentelemetry.io/collector/config"
10+
"go.opentelemetry.io/collector/exporter/exporterhelper"
11+
)
12+
13+
var (
14+
typeStr config.Type = "googlecloudlogging"
15+
defaultTimeout = 12 * time.Second
16+
)
17+
18+
func NewFactory() component.ExporterFactory {
19+
return component.NewExporterFactory(
20+
typeStr,
21+
createDefaultConfig,
22+
component.WithLogsExporter(createLogsExporter))
23+
}
24+
25+
// createDefaultConfig creates the default configuration for exporter.
26+
func createDefaultConfig() config.Exporter {
27+
return &Config{
28+
ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)),
29+
TimeoutSettings: exporterhelper.TimeoutSettings{Timeout: defaultTimeout},
30+
RetrySettings: exporterhelper.NewDefaultRetrySettings(),
31+
QueueSettings: exporterhelper.NewDefaultQueueSettings(),
32+
UserAgent: "opentelemetry-collector-contrib {{version}}",
33+
}
34+
}
35+
36+
// createLogsExporter creates the Google Cloud Logging exporter
37+
func createLogsExporter(_ context.Context, params component.ExporterCreateSettings, config config.Exporter) (component.LogsExporter, error) {
38+
expConfig, ok := config.(*Config)
39+
if !ok {
40+
return nil, errors.New("invalid configuration type; can't cast to googlecloudloggingexporter.Config")
41+
}
42+
return newCloudLoggingExporter(expConfig, params)
43+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module github.com/open-telemetry/opentelemetry-collector-contrib/exporter/googlecloudloggingexporter
2+
3+
go 1.17
4+
5+
replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal => ../../internal/coreinternal
6+
7+
require (
8+
cloud.google.com/go/logging v1.4.2
9+
github.com/google/uuid v1.3.0
10+
go.opentelemetry.io/collector v0.45.1-0.20220223001941-c9c253193a75
11+
go.opentelemetry.io/collector/model v0.45.1-0.20220222185228-27f7607ca13a
12+
go.uber.org/zap v1.21.0
13+
)
14+
15+
require (
16+
cloud.google.com/go v0.81.0 // indirect
17+
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
18+
github.com/fsnotify/fsnotify v1.5.1 // indirect
19+
github.com/gogo/protobuf v1.3.2 // indirect
20+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
21+
github.com/golang/protobuf v1.5.2 // indirect
22+
github.com/google/go-cmp v0.5.7 // indirect
23+
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
24+
github.com/jstemmer/go-junit-report v0.9.1 // indirect
25+
github.com/knadh/koanf v1.4.0 // indirect
26+
github.com/mitchellh/copystructure v1.2.0 // indirect
27+
github.com/mitchellh/mapstructure v1.4.3 // indirect
28+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
29+
github.com/pelletier/go-toml v1.9.4 // indirect
30+
github.com/pkg/errors v0.9.1 // indirect
31+
github.com/spf13/cast v1.4.1 // indirect
32+
go.opencensus.io v0.23.0 // indirect
33+
go.opentelemetry.io/otel v1.4.1 // indirect
34+
go.opentelemetry.io/otel/metric v0.27.0 // indirect
35+
go.opentelemetry.io/otel/trace v1.4.1 // indirect
36+
go.uber.org/atomic v1.9.0 // indirect
37+
go.uber.org/multierr v1.7.0 // indirect
38+
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
39+
golang.org/x/mod v0.4.2 // indirect
40+
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
41+
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
42+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
43+
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
44+
golang.org/x/text v0.3.7 // indirect
45+
golang.org/x/tools v0.1.5 // indirect
46+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
47+
google.golang.org/api v0.46.0 // indirect
48+
google.golang.org/appengine v1.6.7 // indirect
49+
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
50+
google.golang.org/grpc v1.44.0 // indirect
51+
google.golang.org/protobuf v1.27.1 // indirect
52+
)

0 commit comments

Comments
 (0)