diff --git a/apmpackage/apm/changelog.yml b/apmpackage/apm/changelog.yml index 0d72ce3737..3502b5febe 100644 --- a/apmpackage/apm/changelog.yml +++ b/apmpackage/apm/changelog.yml @@ -38,6 +38,9 @@ - description: updated ingest pipelines to reject events from apm-servers newer than installed integration type: enhancement link: https://github.com/elastic/apm-server/pull/6791 + - description: added event.{outcome,severity} and log.level to app_logs data stream + type: enhancement + link: https://github.com/elastic/apm-server/pull/6791 - version: "7.16.1" changes: - description: Added `agent_config_applied` mapping to `metrics-apm.internal` data stream. diff --git a/apmpackage/apm/data_stream/app_logs/fields/ecs.yml b/apmpackage/apm/data_stream/app_logs/fields/ecs.yml index 5c2fe51a94..c705fd572b 100644 --- a/apmpackage/apm/data_stream/app_logs/fields/ecs.yml +++ b/apmpackage/apm/data_stream/app_logs/fields/ecs.yml @@ -58,6 +58,10 @@ name: ecs.version - external: ecs name: event.outcome +- external: ecs + name: event.severity +- external: ecs + name: event.action - external: ecs name: host.architecture - external: ecs @@ -79,6 +83,8 @@ - external: ecs name: labels dynamic: true +- external: ecs + name: log.level - external: ecs name: message - external: ecs diff --git a/beater/otlp/grpc.go b/beater/otlp/grpc.go index 4f12ec9f85..8a59fd7a7d 100644 --- a/beater/otlp/grpc.go +++ b/beater/otlp/grpc.go @@ -44,18 +44,22 @@ var ( gRPCMetricsMonitoringMap = request.MonitoringMapForRegistry(gRPCMetricsRegistry, monitoringKeys) gRPCTracesRegistry = monitoring.Default.NewRegistry("apm-server.otlp.grpc.traces") gRPCTracesMonitoringMap = request.MonitoringMapForRegistry(gRPCTracesRegistry, monitoringKeys) + gRPCLogsRegistry = monitoring.Default.NewRegistry("apm-server.otlp.grpc.logs") + gRPCLogsMonitoringMap = request.MonitoringMapForRegistry(gRPCLogsRegistry, monitoringKeys) // RegistryMonitoringMaps provides mappings from the fully qualified gRPC // method name to its respective monitoring map. RegistryMonitoringMaps = map[string]map[request.ResultID]*monitoring.Int{ metricsFullMethod: gRPCMetricsMonitoringMap, tracesFullMethod: gRPCTracesMonitoringMap, + logsFullMethod: gRPCLogsMonitoringMap, } ) const ( metricsFullMethod = "/opentelemetry.proto.collector.metrics.v1.MetricsService/Export" tracesFullMethod = "/opentelemetry.proto.collector.trace.v1.TraceService/Export" + logsFullMethod = "/opentelemetry.proto.collector.logs.v1.LogsService/Export" ) func init() { @@ -68,6 +72,7 @@ func MethodAuthenticators(authenticator *auth.Authenticator) map[string]intercep return map[string]interceptors.MethodAuthenticator{ metricsFullMethod: metadataMethodAuthenticator, tracesFullMethod: metadataMethodAuthenticator, + logsFullMethod: metadataMethodAuthenticator, } } @@ -86,6 +91,9 @@ func RegisterGRPCServices(grpcServer *grpc.Server, processor model.BatchProcesso if err := otlpreceiver.RegisterMetricsReceiver(context.Background(), consumer, grpcServer); err != nil { return errors.Wrap(err, "failed to register OTLP metrics receiver") } + if err := otlpreceiver.RegisterLogsReceiver(context.Background(), consumer, grpcServer); err != nil { + return errors.Wrap(err, "failed to register OTLP logs receiver") + } return nil } diff --git a/beater/otlp/grpc_test.go b/beater/otlp/grpc_test.go index 917a7f76c3..c1c2edf9fd 100644 --- a/beater/otlp/grpc_test.go +++ b/beater/otlp/grpc_test.go @@ -133,6 +133,53 @@ func TestConsumeMetrics(t *testing.T) { }, actual) } +func TestConsumeLogs(t *testing.T) { + var batches []model.Batch + var reportError error + var batchProcessor model.ProcessBatchFunc = func(ctx context.Context, batch *model.Batch) error { + batches = append(batches, *batch) + return reportError + } + + conn := newServer(t, batchProcessor) + client := otlpgrpc.NewLogsClient(conn) + + // Send a minimal log to verify that everything is connected properly. + // + // We intentionally do not check the published event contents; those are + // tested in processor/otel. + logs := pdata.NewLogs() + log := logs.ResourceLogs().AppendEmpty().InstrumentationLibraryLogs().AppendEmpty().Logs().AppendEmpty() + log.SetName("log_name") + + _, err := client.Export(context.Background(), logs) + assert.NoError(t, err) + require.Len(t, batches, 1) + + reportError = errors.New("failed to publish events") + _, err = client.Export(context.Background(), logs) + assert.Error(t, err) + errStatus := status.Convert(err) + assert.Equal(t, "failed to publish events", errStatus.Message()) + require.Len(t, batches, 2) + assert.Len(t, batches[0], 1) + assert.Len(t, batches[1], 1) + + actual := map[string]interface{}{} + monitoring.GetRegistry("apm-server.otlp.grpc.logs").Do(monitoring.Full, func(key string, value interface{}) { + actual[key] = value + }) + assert.Equal(t, map[string]interface{}{ + "request.count": int64(2), + "response.count": int64(2), + "response.errors.count": int64(1), + "response.valid.count": int64(1), + "response.errors.ratelimit": int64(0), + "response.errors.timeout": int64(0), + "response.errors.unauthorized": int64(0), + }, actual) +} + func newServer(t *testing.T, batchProcessor model.BatchProcessor) *grpc.ClientConn { lis, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) diff --git a/changelogs/head.asciidoc b/changelogs/head.asciidoc index b3b8d31fb7..b1451570a2 100644 --- a/changelogs/head.asciidoc +++ b/changelogs/head.asciidoc @@ -41,6 +41,7 @@ https://github.com/elastic/apm-server/compare/7.15\...master[View commits] - Modify default standalone apm-server config values to be more in line with the default managed apm-server values {pull}6675[6675] - APM Server is now using a new Elasticsearch output implementation {pull}6656[6656] - Standalone apm-server can now fetch source maps uploaded to Kibana, when `apm-server.kibana` is configured {pull}6447[6447] +- APM Server now has beta support to receive OpenTelemetry Logs on the OTLP/GRPC receiver {pull}6768[6768] [float] ==== Licensing Changes diff --git a/docs/open-telemetry.asciidoc b/docs/open-telemetry.asciidoc index ec453be426..ccfe3a2cdc 100644 --- a/docs/open-telemetry.asciidoc +++ b/docs/open-telemetry.asciidoc @@ -401,4 +401,4 @@ Here is an example of AWS Lambda Node.js function managed with Terraform and the [[open-telemetry-logs-limitations]] ===== OpenTelemetry logs -* OpenTelemetry logs are not yet supported https://github.com/elastic/apm-server/issues/5491[#5491] +* OpenTelemetry logs are supported with **beta support** from 8.0 https://github.com/elastic/apm-server/pull/6768[#6768]. diff --git a/model/apmevent.go b/model/apmevent.go index da42ba8cbe..1daf83a99c 100644 --- a/model/apmevent.go +++ b/model/apmevent.go @@ -30,9 +30,6 @@ import ( // Exactly one of the event fields should be non-nil. type APMEvent struct { // DataStream optionally holds data stream identifiers. - // - // This will have the zero value when APM Server is run - // in standalone mode. DataStream DataStream ECSVersion string @@ -59,6 +56,7 @@ type APMEvent struct { Child Child HTTP HTTP FAAS FAAS + Log Log // Timestamp holds the event timestamp. // @@ -152,5 +150,6 @@ func (e *APMEvent) BeatEvent(ctx context.Context) beat.Event { fields.maybeSetString("message", e.Message) fields.maybeSetMapStr("http", e.HTTP.fields()) fields.maybeSetMapStr("faas", e.FAAS.fields()) + fields.maybeSetMapStr("log", e.Log.fields()) return event } diff --git a/model/error.go b/model/error.go index 5353959aac..7488342a67 100644 --- a/model/error.go +++ b/model/error.go @@ -38,7 +38,7 @@ type Error struct { Custom common.MapStr Exception *Exception - Log *Log + Log *ErrorLog } type Exception struct { @@ -52,7 +52,7 @@ type Exception struct { Cause []Exception } -type Log struct { +type ErrorLog struct { Message string Level string ParamMessage string diff --git a/model/error_test.go b/model/error_test.go index ce61188c33..f4cdb28ab4 100644 --- a/model/error_test.go +++ b/model/error_test.go @@ -36,8 +36,8 @@ func (e *Exception) withCode(code string) *Exception { return e } -func baseLog() *Log { - return &Log{Message: "error log message"} +func baseLog() *ErrorLog { + return &ErrorLog{Message: "error log message"} } func TestHandleExceptionTree(t *testing.T) { @@ -132,7 +132,7 @@ func TestEventFields(t *testing.T) { loggerName := "logger" logMsg := "error log message" paramMsg := "param message" - log := Log{ + log := ErrorLog{ Level: level, Message: logMsg, ParamMessage: paramMsg, diff --git a/model/event.go b/model/event.go index 376c7b623d..50ec54f6a3 100644 --- a/model/event.go +++ b/model/event.go @@ -36,10 +36,20 @@ type Event struct { // Outcome holds the event outcome: "success", "failure", or "unknown". Outcome string + + // Severity holds the numeric severity of the event for log events. + Severity int64 + + // Severity holds the action captured by the event for log events. + Action string } func (e *Event) fields() common.MapStr { var fields mapStr fields.maybeSetString("outcome", e.Outcome) + fields.maybeSetString("action", e.Action) + if e.Severity > 0 { + fields.set("severity", e.Severity) + } return common.MapStr(fields) } diff --git a/model/log.go b/model/log.go index 97f44c6a54..2985d92643 100644 --- a/model/log.go +++ b/model/log.go @@ -17,6 +17,8 @@ package model +import "github.com/elastic/beats/v7/libbeat/common" + const ( AppLogsDataset = "apm.app" ) @@ -25,3 +27,17 @@ var ( // LogProcessor is the Processor value that should be assigned to log events. LogProcessor = Processor{Name: "log", Event: "log"} ) + +// Log holds information about a log, as defined by ECS. +// +// https://www.elastic.co/guide/en/ecs/current/ecs-log.html +type Log struct { + // Level holds the log level of the log event. + Level string +} + +func (e Log) fields() common.MapStr { + var fields mapStr + fields.maybeSetString("level", e.Level) + return common.MapStr(fields) +} diff --git a/model/modeldecoder/rumv3/decoder.go b/model/modeldecoder/rumv3/decoder.go index b604cebad4..433051c2ed 100644 --- a/model/modeldecoder/rumv3/decoder.go +++ b/model/modeldecoder/rumv3/decoder.go @@ -210,7 +210,7 @@ func mapToErrorModel(from *errorEvent, event *model.APMEvent) { out.ID = from.ID.Val } if from.Log.IsSet() { - log := model.Log{} + log := model.ErrorLog{} if from.Log.Level.IsSet() { log.Level = from.Log.Level.Val } diff --git a/model/modeldecoder/rumv3/metadata_test.go b/model/modeldecoder/rumv3/metadata_test.go index e49cb4e448..ce65ed9ed4 100644 --- a/model/modeldecoder/rumv3/metadata_test.go +++ b/model/modeldecoder/rumv3/metadata_test.go @@ -82,6 +82,7 @@ func metadataExceptions(keys ...string) func(key string) bool { "Session", "Trace", "URL", + "Log", // Dedicated test for it. "NumericLabels", diff --git a/model/modeldecoder/v2/decoder.go b/model/modeldecoder/v2/decoder.go index 72847e2a93..3ff8ccc168 100644 --- a/model/modeldecoder/v2/decoder.go +++ b/model/modeldecoder/v2/decoder.go @@ -362,7 +362,7 @@ func mapToErrorModel(from *errorEvent, event *model.APMEvent) { out.ID = from.ID.Val } if from.Log.IsSet() { - log := model.Log{} + log := model.ErrorLog{} if from.Log.Level.IsSet() { log.Level = from.Log.Level.Val } diff --git a/model/modeldecoder/v2/metadata_test.go b/model/modeldecoder/v2/metadata_test.go index ffa1b50ca2..3dda9336c0 100644 --- a/model/modeldecoder/v2/metadata_test.go +++ b/model/modeldecoder/v2/metadata_test.go @@ -113,6 +113,10 @@ func isUnmappedMetadataField(key string) bool { "Event", "Event.Duration", "Event.Outcome", + "Event.Severity", + "Event.Action", + "Log", + "Log.Level", "Service.Origin", "Service.Origin.ID", "Service.Origin.Name", diff --git a/model/modelprocessor/culprit_test.go b/model/modelprocessor/culprit_test.go index a514a427b9..95ea9d0da4 100644 --- a/model/modelprocessor/culprit_test.go +++ b/model/modelprocessor/culprit_test.go @@ -40,7 +40,7 @@ func TestSetCulprit(t *testing.T) { }, { input: model.Error{ Culprit: "already_set", - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{{SourcemapUpdated: false, Filename: "foo.go"}}, }, }, @@ -48,7 +48,7 @@ func TestSetCulprit(t *testing.T) { }, { input: model.Error{ Culprit: "already_set", - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{{SourcemapUpdated: true, LibraryFrame: true, Filename: "foo.go"}}, }, }, @@ -56,7 +56,7 @@ func TestSetCulprit(t *testing.T) { }, { input: model.Error{ Culprit: "already_set", - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ {SourcemapUpdated: true, LibraryFrame: true, Filename: "foo.go"}, {SourcemapUpdated: true, LibraryFrame: false, Filename: "foo2.go"}, @@ -67,7 +67,7 @@ func TestSetCulprit(t *testing.T) { }, { input: model.Error{ Culprit: "already_set", - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{{SourcemapUpdated: true, LibraryFrame: true, Filename: "foo.go"}}, }, Exception: &model.Exception{ @@ -77,7 +77,7 @@ func TestSetCulprit(t *testing.T) { culprit: "foo2.go", }, { input: model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ {SourcemapUpdated: true, Classname: "AbstractFactoryManagerBean", Function: "toString"}, }, diff --git a/model/modelprocessor/errormessage_test.go b/model/modelprocessor/errormessage_test.go index 2d75988242..1dee8137df 100644 --- a/model/modelprocessor/errormessage_test.go +++ b/model/modelprocessor/errormessage_test.go @@ -35,20 +35,20 @@ func TestSetErrorMessage(t *testing.T) { input: model.Error{}, message: "", }, { - input: model.Error{Log: &model.Log{Message: "log_message"}}, + input: model.Error{Log: &model.ErrorLog{Message: "log_message"}}, message: "log_message", }, { input: model.Error{Exception: &model.Exception{Message: "exception_message"}}, message: "exception_message", }, { input: model.Error{ - Log: &model.Log{}, + Log: &model.ErrorLog{}, Exception: &model.Exception{Message: "exception_message"}, }, message: "exception_message", }, { input: model.Error{ - Log: &model.Log{Message: "log_message"}, + Log: &model.ErrorLog{Message: "log_message"}, Exception: &model.Exception{Message: "exception_message"}, }, message: "log_message", diff --git a/model/modelprocessor/excludefromgrouping_test.go b/model/modelprocessor/excludefromgrouping_test.go index 2399301ee2..c8097bb728 100644 --- a/model/modelprocessor/excludefromgrouping_test.go +++ b/model/modelprocessor/excludefromgrouping_test.go @@ -60,7 +60,7 @@ func TestSetExcludeFromGrouping(t *testing.T) { }, { input: model.Batch{{ Error: &model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ {Filename: "foo.go"}, }, @@ -82,7 +82,7 @@ func TestSetExcludeFromGrouping(t *testing.T) { }}, output: model.Batch{{ Error: &model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ {ExcludeFromGrouping: true, Filename: "foo.go"}, }, diff --git a/model/modelprocessor/groupingkey_test.go b/model/modelprocessor/groupingkey_test.go index d6339d0aad..4ddfd711b5 100644 --- a/model/modelprocessor/groupingkey_test.go +++ b/model/modelprocessor/groupingkey_test.go @@ -43,7 +43,7 @@ func TestSetGroupingKey(t *testing.T) { Exception: &model.Exception{ Type: "exception_type", }, - Log: &model.Log{ + Log: &model.ErrorLog{ ParamMessage: "log_parammessage", }, }, @@ -72,7 +72,7 @@ func TestSetGroupingKey(t *testing.T) { }, }}, }, - Log: &model.Log{Stacktrace: model.Stacktrace{{Filename: "abc"}}}, // ignored + Log: &model.ErrorLog{Stacktrace: model.Stacktrace{{Filename: "abc"}}}, // ignored }, groupingKey: hashStrings( "module", "func_1", "filename", "func_2", "classname", "func_4", "func_5", "func_6", @@ -80,7 +80,7 @@ func TestSetGroupingKey(t *testing.T) { }, "log_stacktrace": { input: model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{{Function: "function"}}, }, }, @@ -99,13 +99,13 @@ func TestSetGroupingKey(t *testing.T) { Message: "message_4", }}, }, - Log: &model.Log{Message: "log_message"}, // ignored + Log: &model.ErrorLog{Message: "log_message"}, // ignored }, groupingKey: hashStrings("message_1", "message_2", "message_3", "message_4"), }, "log_message": { input: model.Error{ - Log: &model.Log{Message: "log_message"}, // ignored + Log: &model.ErrorLog{Message: "log_message"}, // ignored }, groupingKey: hashStrings("log_message"), }, diff --git a/model/modelprocessor/libraryframe_test.go b/model/modelprocessor/libraryframe_test.go index dcf3d8f609..3e86d8d27f 100644 --- a/model/modelprocessor/libraryframe_test.go +++ b/model/modelprocessor/libraryframe_test.go @@ -62,7 +62,7 @@ func TestSetLibraryFrames(t *testing.T) { }, { input: model.Batch{{ Error: &model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ {LibraryFrame: true, Filename: "foo.go"}, {LibraryFrame: false, AbsPath: "foobar.go"}, @@ -87,7 +87,7 @@ func TestSetLibraryFrames(t *testing.T) { }}, output: model.Batch{{ Error: &model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ {LibraryFrame: true, Filename: "foo.go", Original: model.Original{LibraryFrame: true}}, {LibraryFrame: true, AbsPath: "foobar.go", Original: model.Original{LibraryFrame: false}}, diff --git a/processor/otel/logs.go b/processor/otel/logs.go new file mode 100644 index 0000000000..a1538edec3 --- /dev/null +++ b/processor/otel/logs.go @@ -0,0 +1,136 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +// Portions copied from OpenTelemetry Collector (contrib), from the +// elastic exporter. +// +// Copyright 2020, 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 otel + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/model/otlp" + "go.opentelemetry.io/collector/model/pdata" + + apmserverlogs "github.com/elastic/apm-server/log" + "github.com/elastic/apm-server/model" + "github.com/elastic/beats/v7/libbeat/logp" +) + +var jsonLogsMarshaler = otlp.NewJSONLogsMarshaler() + +func (c *Consumer) ConsumeLogs(ctx context.Context, logs pdata.Logs) error { + receiveTimestamp := time.Now() + logger := logp.NewLogger(apmserverlogs.Otel) + if logger.IsDebug() { + data, err := jsonLogsMarshaler.MarshalLogs(logs) + if err != nil { + logger.Debug(err) + } else { + logger.Debug(string(data)) + } + } + resourceLogs := logs.ResourceLogs() + batch := make(model.Batch, 0, resourceLogs.Len()) + for i := 0; i < resourceLogs.Len(); i++ { + c.convertResourceLogs(resourceLogs.At(i), receiveTimestamp, &batch) + } + return c.Processor.ProcessBatch(ctx, &batch) +} + +func (c *Consumer) convertResourceLogs(resourceLogs pdata.ResourceLogs, receiveTimestamp time.Time, out *model.Batch) { + var timeDelta time.Duration + resource := resourceLogs.Resource() + baseEvent := model.APMEvent{Processor: model.LogProcessor} + translateResourceMetadata(resource, &baseEvent) + if exportTimestamp, ok := exportTimestamp(resource); ok { + timeDelta = receiveTimestamp.Sub(exportTimestamp) + } + instrumentationLibraryLogs := resourceLogs.InstrumentationLibraryLogs() + for i := 0; i < instrumentationLibraryLogs.Len(); i++ { + c.convertInstrumentationLibraryLogs(instrumentationLibraryLogs.At(i), baseEvent, timeDelta, out) + } +} + +func (c *Consumer) convertInstrumentationLibraryLogs( + in pdata.InstrumentationLibraryLogs, + baseEvent model.APMEvent, + timeDelta time.Duration, + out *model.Batch, +) { + otelLogs := in.Logs() + for i := 0; i < otelLogs.Len(); i++ { + event := c.convertLogRecord(otelLogs.At(i), baseEvent, timeDelta) + *out = append(*out, event) + } +} + +func (c *Consumer) convertLogRecord( + record pdata.LogRecord, + baseEvent model.APMEvent, + timeDelta time.Duration, +) model.APMEvent { + event := baseEvent + event.Timestamp = record.Timestamp().AsTime().Add(timeDelta) + event.Event.Severity = int64(record.SeverityNumber()) + event.Event.Action = record.Name() + event.Log.Level = record.SeverityText() + if body := record.Body(); body.Type() != pdata.AttributeValueTypeNull { + event.Message = body.AsString() + if body.Type() == pdata.AttributeValueTypeMap { + setLabels(body.MapVal(), &event) + } + } + if traceID := record.TraceID(); !traceID.IsEmpty() { + event.Trace.ID = traceID.HexString() + } + if spanID := record.SpanID(); !spanID.IsEmpty() { + if event.Span == nil { + event.Span = &model.Span{} + } + event.Span.ID = spanID.HexString() + } + if attrs := record.Attributes(); attrs.Len() > 0 { + setLabels(attrs, &event) + } + return event +} + +func setLabels(m pdata.AttributeMap, event *model.APMEvent) { + if event.Labels == nil || event.NumericLabels == nil { + initEventLabels(event) + } + m.Range(func(k string, v pdata.AttributeValue) bool { + setLabel(k, event, ifaceAttributeValue(v)) + return true + }) +} diff --git a/processor/otel/logs_test.go b/processor/otel/logs_test.go new file mode 100644 index 0000000000..9a571b7fbe --- /dev/null +++ b/processor/otel/logs_test.go @@ -0,0 +1,150 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +// Portions copied from OpenTelemetry Collector (contrib), from the +// elastic exporter. +// +// Copyright 2020, 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 otel_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/model/pdata" + semconv "go.opentelemetry.io/collector/model/semconv/v1.5.0" + + "github.com/elastic/apm-server/model" + "github.com/elastic/apm-server/processor/otel" +) + +func TestConsumerConsumeLogs(t *testing.T) { + t.Run("empty", func(t *testing.T) { + var processor model.ProcessBatchFunc = func(_ context.Context, batch *model.Batch) error { + assert.Empty(t, batch) + return nil + } + + consumer := otel.Consumer{Processor: processor} + logs := pdata.NewLogs() + assert.NoError(t, consumer.ConsumeLogs(context.Background(), logs)) + }) + + commonEvent := model.APMEvent{ + Processor: model.LogProcessor, + Agent: model.Agent{ + Name: "otlp/go", + Version: "unknown", + }, + Service: model.Service{ + Name: "unknown", + Language: model.Language{Name: "go"}, + }, + Message: "a random log message", + Event: model.Event{ + Severity: int64(pdata.SeverityNumberINFO), + Action: "doOperation()", + }, + Log: model.Log{Level: "Info"}, + Span: &model.Span{ID: "0200000000000000"}, + Trace: model.Trace{ID: "01000000000000000000000000000000"}, + Labels: model.Labels{ + "key": model.LabelValue{Value: "value"}, + }, + NumericLabels: model.NumericLabels{ + "numeric_key": model.NumericLabelValue{Value: 1234}, + }, + } + test := func(name string, body interface{}, expectedMessage string) { + t.Run(name, func(t *testing.T) { + logs := newLogs(body) + + var processed model.Batch + var processor model.ProcessBatchFunc = func(_ context.Context, batch *model.Batch) error { + if processed != nil { + panic("already processes batch") + } + processed = *batch + assert.NotNil(t, processed[0].Timestamp) + processed[0].Timestamp = time.Time{} + return nil + } + consumer := otel.Consumer{Processor: processor} + assert.NoError(t, consumer.ConsumeLogs(context.Background(), logs)) + + expected := commonEvent + expected.Message = expectedMessage + assert.Equal(t, model.Batch{expected}, processed) + }) + } + test("string_body", "a random log message", "a random log message") + test("int_body", 1234, "1234") + test("float_body", 1234.1234, "1234.1234") + test("bool_body", true, "true") + // TODO(marclop): How to test map body +} + +func newLogs(body interface{}) pdata.Logs { + logs := pdata.NewLogs() + resourceLogs := logs.ResourceLogs().AppendEmpty() + logs.ResourceLogs().At(0).Resource().Attributes().InitFromMap(map[string]pdata.AttributeValue{ + semconv.AttributeTelemetrySDKLanguage: pdata.NewAttributeValueString("go"), + }) + instrumentationLogs := resourceLogs.InstrumentationLibraryLogs().AppendEmpty() + otelLog := instrumentationLogs.Logs().AppendEmpty() + otelLog.SetTraceID(pdata.NewTraceID([16]byte{1})) + otelLog.SetSpanID(pdata.NewSpanID([8]byte{2})) + otelLog.SetName("doOperation()") + otelLog.SetSeverityNumber(pdata.SeverityNumberINFO) + otelLog.SetSeverityText("Info") + otelLog.SetTimestamp(pdata.NewTimestampFromTime(time.Now())) + otelLog.Attributes().InitFromMap(map[string]pdata.AttributeValue{ + "key": pdata.NewAttributeValueString("value"), + "numeric_key": pdata.NewAttributeValueDouble(1234), + }) + + switch b := body.(type) { + case string: + otelLog.Body().SetStringVal(b) + case int: + otelLog.Body().SetIntVal(int64(b)) + case float64: + otelLog.Body().SetDoubleVal(b) + case bool: + otelLog.Body().SetBoolVal(b) + // case map[string]string: + // TODO(marclop) figure out how to set the body since it cannot be set + // as a map. + // otelLog.Body() + } + return logs +} diff --git a/processor/otel/traces.go b/processor/otel/traces.go index 1b052d8c62..53602f7f95 100644 --- a/processor/otel/traces.go +++ b/processor/otel/traces.go @@ -925,7 +925,7 @@ func convertJaegerErrorSpanEvent(logger *logp.Logger, event pdata.SpanEvent, apm } e := &model.Error{} if logMessage != "" { - e.Log = &model.Log{Message: logMessage} + e.Log = &model.ErrorLog{Message: logMessage} } if exMessage != "" || exType != "" { e.Exception = &model.Exception{ diff --git a/sourcemap/processor_test.go b/sourcemap/processor_test.go index 4121d575bb..ca5bae21fd 100644 --- a/sourcemap/processor_test.go +++ b/sourcemap/processor_test.go @@ -147,7 +147,7 @@ func TestBatchProcessor(t *testing.T) { error2 := model.APMEvent{ Service: service, Error: &model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{{ AbsPath: "bundle.js", Lineno: newInt(originalLinenoWithoutFilename), @@ -197,7 +197,7 @@ func TestBatchProcessor(t *testing.T) { }, }, span2.Span) assert.Equal(t, &model.Error{ - Log: &model.Log{ + Log: &model.ErrorLog{ Stacktrace: model.Stacktrace{ cloneFrame(mappedFrameWithoutFilename), }, diff --git a/systemtest/approvals/TestOTLPGRPCLogs.approved.json b/systemtest/approvals/TestOTLPGRPCLogs.approved.json new file mode 100644 index 0000000000..9370b0a076 --- /dev/null +++ b/systemtest/approvals/TestOTLPGRPCLogs.approved.json @@ -0,0 +1,57 @@ +{ + "events": [ + { + "@timestamp": "1970-01-01T00:00:01.000Z", + "agent": { + "name": "otlp/go", + "version": "unknown" + }, + "data_stream.dataset": "apm.app", + "data_stream.namespace": "default", + "data_stream.type": "logs", + "ecs": { + "version": "dynamic" + }, + "event": { + "action": "doOperation()", + "agent_id_status": "missing", + "ingested": "dynamic", + "severity": 9 + }, + "labels": { + "key": "value" + }, + "log": { + "level": "Info" + }, + "message": "a log message", + "numeric_labels": { + "numeric_key": 1234 + }, + "observer": { + "ephemeral_id": "dynamic", + "hostname": "dynamic", + "id": "dynamic", + "type": "apm-server", + "version": "dynamic", + "version_major": "dynamic" + }, + "processor": { + "event": "log", + "name": "log" + }, + "service": { + "language": { + "name": "go" + }, + "name": "unknown" + }, + "span": { + "id": "0200000000000000" + }, + "trace": { + "id": "01000000000000000000000000000000" + } + } + ] +} diff --git a/systemtest/go.mod b/systemtest/go.mod index 4bfb4fea58..51f1d72048 100644 --- a/systemtest/go.mod +++ b/systemtest/go.mod @@ -29,7 +29,7 @@ require ( go.uber.org/zap v1.15.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 - google.golang.org/grpc v1.40.0 + google.golang.org/grpc v1.42.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) @@ -75,6 +75,7 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.1.1 // indirect go.opencensus.io v0.22.3 // indirect + go.opentelemetry.io/collector/model v0.34.0 go.opentelemetry.io/otel/internal/metric v0.23.0 // indirect go.opentelemetry.io/proto/otlp v0.9.0 // indirect go.uber.org/atomic v1.6.0 // indirect @@ -85,7 +86,7 @@ require ( golang.org/x/text v0.3.6 // indirect golang.org/x/tools v0.1.5 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect + google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect diff --git a/systemtest/go.sum b/systemtest/go.sum index 2e5a114212..96420e2818 100644 --- a/systemtest/go.sum +++ b/systemtest/go.sum @@ -104,7 +104,11 @@ github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= @@ -205,6 +209,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -715,6 +720,10 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/collector/model v0.34.0 h1:JmuBhBvX0l0bYDBAG9GtJVQKcIQRspPLgHfZqgHLpJc= +go.opentelemetry.io/collector/model v0.34.0/go.mod h1:+7YCSjJG+MqiIFjauzt7oM2qkqBsaJWh5hcsO4fwsAc= +go.opentelemetry.io/collector/model v0.40.0 h1:UgdWfBnJBQL4atFv6LhlYq67Ts/vFDbwGknvsIsm/g8= +go.opentelemetry.io/collector/model v0.40.0/go.mod h1:dXqjAeml+cB+YzJ3kUnd3v5/JvGAKl3MqHXfgSWRIo8= go.opentelemetry.io/otel v1.0.0-RC3/go.mod h1:Ka5j3ua8tZs4Rkq4Ex3hwgBgOchyPVq5S6P2lz//nKQ= go.opentelemetry.io/otel v1.0.0 h1:qTTn6x71GVBvoafHK/yaRUmFzI4LcONZD0/kXxl5PHI= go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg= @@ -940,6 +949,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -997,6 +1007,7 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1045,6 +1056,8 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 h1:pc16UedxnxXXtGxHCSUhafAoVHQZ0yXl8ZelMH4EETc= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1060,8 +1073,11 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/systemtest/otlp_test.go b/systemtest/otlp_test.go index f7efc9bf46..0f6787becb 100644 --- a/systemtest/otlp_test.go +++ b/systemtest/otlp_test.go @@ -25,6 +25,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "go.opentelemetry.io/collector/model/otlpgrpc" + "go.opentelemetry.io/collector/model/pdata" + semconv "go.opentelemetry.io/collector/model/semconv/v1.5.0" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric" @@ -42,6 +45,7 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -135,6 +139,28 @@ func TestOTLPGRPCMetrics(t *testing.T) { assert.True(t, gjson.GetBytes(doc.RawSource, "beats_stats.metrics.apm-server.otlp.grpc.metrics.consumer").Exists()) } +func TestOTLPGRPCLogs(t *testing.T) { + systemtest.CleanupElasticsearch(t) + srv := apmservertest.NewServer(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + conn, err := grpc.Dial(serverAddr(srv), grpc.WithInsecure(), grpc.WithBlock()) + require.NoError(t, err) + defer conn.Close() + + logsClient := otlpgrpc.NewLogsClient(conn) + + logs := newLogs("a log message") + _, err = logsClient.Export(ctx, logs) + require.NoError(t, err) + + result := systemtest.Elasticsearch.ExpectDocs(t, "logs-apm*", estest.TermQuery{ + Field: "processor.event", Value: "log", + }) + systemtest.ApproveEvents(t, t.Name(), result.Hits.Hits) +} + func TestOTLPGRPCAuth(t *testing.T) { systemtest.CleanupElasticsearch(t) srv := apmservertest.NewUnstartedServer(t) @@ -429,3 +455,35 @@ func (m *idGeneratorFuncs) NewIDs(ctx context.Context) (trace.TraceID, trace.Spa func (m *idGeneratorFuncs) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID { return m.newSpanID(ctx, traceID) } + +func newLogs(body interface{}) pdata.Logs { + logs := pdata.NewLogs() + resourceLogs := logs.ResourceLogs().AppendEmpty() + logs.ResourceLogs().At(0).Resource().Attributes().InitFromMap(map[string]pdata.AttributeValue{ + semconv.AttributeTelemetrySDKLanguage: pdata.NewAttributeValueString("go"), + }) + instrumentationLogs := resourceLogs.InstrumentationLibraryLogs().AppendEmpty() + otelLog := instrumentationLogs.Logs().AppendEmpty() + otelLog.SetTraceID(pdata.NewTraceID([16]byte{1})) + otelLog.SetSpanID(pdata.NewSpanID([8]byte{2})) + otelLog.SetName("doOperation()") + otelLog.SetSeverityNumber(pdata.SeverityNumberINFO) + otelLog.SetSeverityText("Info") + otelLog.SetTimestamp(pdata.NewTimestampFromTime(time.Unix(1, 0))) + otelLog.Attributes().InitFromMap(map[string]pdata.AttributeValue{ + "key": pdata.NewAttributeValueString("value"), + "numeric_key": pdata.NewAttributeValueDouble(1234), + }) + + switch b := body.(type) { + case string: + otelLog.Body().SetStringVal(b) + case int: + otelLog.Body().SetIntVal(int64(b)) + case float64: + otelLog.Body().SetDoubleVal(float64(b)) + case bool: + otelLog.Body().SetBoolVal(b) + } + return logs +}