diff --git a/pkg/translator/faro/faro_to_logs.go b/pkg/translator/faro/faro_to_logs.go new file mode 100644 index 0000000..ecc1933 --- /dev/null +++ b/pkg/translator/faro/faro_to_logs.go @@ -0,0 +1,99 @@ +package faro + +import ( + "context" + "strconv" + "time" + + "github.com/go-logfmt/logfmt" + faroTypes "github.com/grafana/faro/pkg/go" + "github.com/grafana/faro/pkg/translator/faro/internal" + "github.com/zeebo/xxh3" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +type kvTime struct { + kv *internal.KeyVal + ts time.Time + kind faroTypes.Kind + hash uint64 +} + +// TranslateToLogs converts faro.Payload into Logs pipeline data +func TranslateToLogs(ctx context.Context, payload faroTypes.Payload) (*plog.Logs, error) { + ctx, span := otel.Tracer("").Start(ctx, "TranslateToLogs") + defer span.End() + var kvList []*kvTime + + for _, logItem := range payload.Logs { + kvList = append(kvList, &kvTime{ + kv: internal.LogToKeyVal(logItem), + ts: logItem.Timestamp, + kind: faroTypes.KindLog, + }) + } + for _, exception := range payload.Exceptions { + kvList = append(kvList, &kvTime{ + kv: internal.ExceptionToKeyVal(exception), + ts: exception.Timestamp, + kind: faroTypes.KindException, + hash: xxh3.HashString(exception.Value), + }) + } + for _, measurement := range payload.Measurements { + kvList = append(kvList, &kvTime{ + kv: internal.MeasurementToKeyVal(measurement), + ts: measurement.Timestamp, + kind: faroTypes.KindMeasurement, + }) + } + for _, event := range payload.Events { + kvList = append(kvList, &kvTime{ + kv: internal.EventToKeyVal(event), + ts: event.Timestamp, + kind: faroTypes.KindEvent, + }) + } + if len(kvList) == 0 { + return nil, nil + } + span.SetAttributes(attribute.Int("count", len(kvList))) + logs := plog.NewLogs() + meta := internal.MetaToKeyVal(payload.Meta) + resourceAttrs := map[string]any{ + string(semconv.ServiceNameKey): payload.Meta.App.Name, + string(semconv.ServiceVersionKey): payload.Meta.App.Version, + string(semconv.DeploymentEnvironmentKey): payload.Meta.App.Environment, + } + if payload.Meta.App.Namespace != "" { + resourceAttrs[string(semconv.ServiceNamespaceKey)] = payload.Meta.App.Namespace + } + if payload.Meta.App.BundleID != "" { + resourceAttrs["app_bundle_id"] = payload.Meta.App.BundleID + } + rls := logs.ResourceLogs().AppendEmpty() + if err := rls.Resource().Attributes().FromRaw(resourceAttrs); err != nil { + return nil, err + } + sl := rls.ScopeLogs().AppendEmpty() + attrs := pcommon.NewMap() + for _, i := range kvList { + internal.MergeKeyVal(i.kv, meta) + line, err := logfmt.MarshalKeyvals(internal.KeyValToInterfaceSlice(i.kv)...) + if err != nil { + return nil, err + } + logRecord := sl.LogRecords().AppendEmpty() + logRecord.Body().SetStr(string(line)) + attrs.CopyTo(logRecord.Attributes()) + logRecord.Attributes().PutStr("kind", string(i.kind)) + if (i.kind == faroTypes.KindException) && (i.hash != 0) { + logRecord.Attributes().PutStr("hash", strconv.FormatUint(i.hash, 10)) + } + } + return &logs, nil +} diff --git a/pkg/translator/faro/faro_to_logs_test.go b/pkg/translator/faro/faro_to_logs_test.go new file mode 100644 index 0000000..bb21aaa --- /dev/null +++ b/pkg/translator/faro/faro_to_logs_test.go @@ -0,0 +1,212 @@ +package faro + +import ( + "context" + "testing" + + faroTypes "github.com/grafana/faro/pkg/go" + "github.com/grafana/faro/pkg/translator/faro/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/plogtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/plog" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +func TestTranslateToLogs(t *testing.T) { + testcases := []struct { + name string + faroPayload faroTypes.Payload + expectedLogs *plog.Logs + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Empty payload", + faroPayload: faroTypes.Payload{}, + expectedLogs: nil, + wantErr: assert.NoError, + }, + { + name: "Standard payload", + faroPayload: *internal.PayloadFromFile(t, "payload.json"), + expectedLogs: generateLogs(t), + wantErr: assert.NoError, + }, + { + name: "Payload with browser brands as slice", + faroPayload: *internal.PayloadFromFile(t, "payload-browser-brand-slice.json"), + expectedLogs: generateLogsWithBrowserBrandsAsSlice(t), + wantErr: assert.NoError, + }, + { + name: "Payload with browser brands as string", + faroPayload: *internal.PayloadFromFile(t, "payload-browser-brand-string.json"), + expectedLogs: generateLogsWithBrowserBrandsAsString(t), + wantErr: assert.NoError, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + logs, err := TranslateToLogs(context.TODO(), tt.faroPayload) + if !tt.wantErr(t, err) { + return + } + if tt.expectedLogs == nil && assert.Nil(t, logs) { + return + } + + assert.NoError( + t, + plogtest.CompareLogs(*tt.expectedLogs, *logs), + ) + }) + } +} + +func generateLogs(t *testing.T) *plog.Logs { + t.Helper() + logs := plog.NewLogs() + rl := generateResourceLogs(t) + lrs := generateLogRecords(t) + lrs.CopyTo(rl.ScopeLogs().AppendEmpty().LogRecords()) + rl.CopyTo(logs.ResourceLogs().AppendEmpty()) + return &logs +} + +func generateResourceLogs(t *testing.T) plog.ResourceLogs { + t.Helper() + resourceLogs := plog.NewResourceLogs() + err := resourceLogs.Resource().Attributes().FromRaw(map[string]any{ + string(semconv.ServiceNameKey): "testapp", + string(semconv.ServiceVersionKey): "abcdefg", + string(semconv.ServiceNamespaceKey): "testnamespace", + string(semconv.DeploymentEnvironmentKey): "production", + "app_bundle_id": "testBundleId", + }) + require.NoError(t, err) + return resourceLogs +} + +func generateLogRecords(t *testing.T) plog.LogRecordSlice { + t.Helper() + records := plog.NewLogRecordSlice() + + logRecords := []struct { + Body string + Attributes map[string]any + }{ + { + Body: "timestamp=2021-09-30T10:46:17.68Z kind=log message=\"opened pricing page\" level=info context_component=AppRoot context_page=Pricing traceID=abcd spanID=def sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production user_email=geralt@kaermorhen.org user_id=123 user_username=testuser user_attr_foo=bar session_id=abcd session_attr_time_elapsed=100s page_url=https://example.com/page browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false view_name=foobar", + Attributes: map[string]any{ + "kind": "log", + }, + }, + { + Body: "timestamp=2021-09-30T10:46:17.68Z kind=log message=\"loading price list\" level=trace context_component=AppRoot context_page=Pricing traceID=abcd spanID=ghj sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production user_email=geralt@kaermorhen.org user_id=123 user_username=testuser user_attr_foo=bar session_id=abcd session_attr_time_elapsed=100s page_url=https://example.com/page browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false view_name=foobar", + Attributes: map[string]any{ + "kind": "log", + }, + }, + { + Body: "timestamp=2021-09-30T10:46:17.68Z kind=exception type=Error value=\"Cannot read property 'find' of undefined\" stacktrace=\"Error: Cannot read property 'find' of undefined\\n at ? (http://fe:3002/static/js/vendors~main.chunk.js:8639:42)\\n at dispatchAction (http://fe:3002/static/js/vendors~main.chunk.js:268095:9)\\n at scheduleUpdateOnFiber (http://fe:3002/static/js/vendors~main.chunk.js:273726:13)\\n at flushSyncCallbackQueue (http://fe:3002/static/js/vendors~main.chunk.js:263362:7)\\n at flushSyncCallbackQueueImpl (http://fe:3002/static/js/vendors~main.chunk.js:263374:13)\\n at runWithPriority$1 (http://fe:3002/static/js/vendors~main.chunk.js:263325:14)\\n at unstable_runWithPriority (http://fe:3002/static/js/vendors~main.chunk.js:291265:16)\\n at ? (http://fe:3002/static/js/vendors~main.chunk.js:263379:30)\\n at performSyncWorkOnRoot (http://fe:3002/static/js/vendors~main.chunk.js:274126:22)\\n at renderRootSync (http://fe:3002/static/js/vendors~main.chunk.js:274509:11)\\n at workLoopSync (http://fe:3002/static/js/vendors~main.chunk.js:274543:9)\\n at performUnitOfWork (http://fe:3002/static/js/vendors~main.chunk.js:274606:16)\\n at beginWork$1 (http://fe:3002/static/js/vendors~main.chunk.js:275746:18)\\n at beginWork (http://fe:3002/static/js/vendors~main.chunk.js:270944:20)\\n at updateFunctionComponent (http://fe:3002/static/js/vendors~main.chunk.js:269291:24)\\n at renderWithHooks (http://fe:3002/static/js/vendors~main.chunk.js:266969:22)\\n at ? (http://fe:3002/static/js/main.chunk.js:2600:74)\\n at useGetBooksQuery (http://fe:3002/static/js/main.chunk.js:1299:65)\\n at Module.useQuery (http://fe:3002/static/js/vendors~main.chunk.js:8495:85)\\n at useBaseQuery (http://fe:3002/static/js/vendors~main.chunk.js:8656:83)\\n at useDeepMemo (http://fe:3002/static/js/vendors~main.chunk.js:8696:14)\\n at ? (http://fe:3002/static/js/vendors~main.chunk.js:8657:55)\\n at QueryData.execute (http://fe:3002/static/js/vendors~main.chunk.js:7883:47)\\n at QueryData.getExecuteResult (http://fe:3002/static/js/vendors~main.chunk.js:7944:23)\\n at QueryData._this.getQueryResult (http://fe:3002/static/js/vendors~main.chunk.js:7790:19)\\n at new ApolloError (http://fe:3002/static/js/vendors~main.chunk.js:5164:24)\" traceID=abcd spanID=def context_ReactError=\"Annoying Error\" context_component=ReactErrorBoundary sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production user_email=geralt@kaermorhen.org user_id=123 user_username=testuser user_attr_foo=bar session_id=abcd session_attr_time_elapsed=100s page_url=https://example.com/page browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false view_name=foobar", + Attributes: map[string]any{ + "kind": "exception", + "hash": "2735541995122471342", + }, + }, + { + Body: "timestamp=2021-09-30T10:46:17.68Z kind=measurement type=\"page load\" context_hello=world ttfb=14.000000 ttfcp=22.120000 ttfp=20.120000 traceID=abcd spanID=def value_ttfb=14 value_ttfcp=22.12 value_ttfp=20.12 sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production user_email=geralt@kaermorhen.org user_id=123 user_username=testuser user_attr_foo=bar session_id=abcd session_attr_time_elapsed=100s page_url=https://example.com/page browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false view_name=foobar", + Attributes: map[string]any{ + "kind": "measurement", + }, + }, + { + Body: "timestamp=2023-11-16T10:00:55.995Z kind=event event_name=faro.performanceEntry event_domain=browser event_data_connectEnd=3656 event_data_connectStart=337 event_data_decodedBodySize=0 event_data_domainLookupEnd=590 event_data_domainLookupStart=588 event_data_duration=3371 event_data_encodedBodySize=0 event_data_entryType=resource event_data_fetchStart=331 event_data_initiatorType=other event_data_name=https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css.map event_data_nextHopProtocol=h2 event_data_redirectEnd=0 event_data_redirectStart=0 event_data_requestStart=3656 event_data_responseEnd=3702 event_data_responseStart=3690 event_data_secureConnectionStart=3638 event_data_serverTiming=[] event_data_startTime=331 event_data_transferSize=0 event_data_workerStart=0 sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production user_email=geralt@kaermorhen.org user_id=123 user_username=testuser user_attr_foo=bar session_id=abcd session_attr_time_elapsed=100s page_url=https://example.com/page browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false view_name=foobar", + Attributes: map[string]any{ + "kind": "event", + }, + }, + } + + for _, logRecord := range logRecords { + lr := records.AppendEmpty() + lr.Body().SetStr(logRecord.Body) + err := lr.Attributes().FromRaw(logRecord.Attributes) + require.NoError(t, err) + } + + return records +} + +func generateLogsWithBrowserBrandsAsSlice(t *testing.T) *plog.Logs { + t.Helper() + logs := plog.NewLogs() + rl := generateResourceLogs(t) + lrs := generateLogRecordsWithBrowserBrandsAsSlice(t) + lrs.CopyTo(rl.ScopeLogs().AppendEmpty().LogRecords()) + rl.CopyTo(logs.ResourceLogs().AppendEmpty()) + return &logs +} + +func generateLogsWithBrowserBrandsAsString(t *testing.T) *plog.Logs { + t.Helper() + logs := plog.NewLogs() + rl := generateResourceLogs(t) + lrs := generateLogRecordsWithBrowserBrandsAsString(t) + lrs.CopyTo(rl.ScopeLogs().AppendEmpty().LogRecords()) + rl.CopyTo(logs.ResourceLogs().AppendEmpty()) + return &logs +} + +func generateLogRecordsWithBrowserBrandsAsSlice(t *testing.T) plog.LogRecordSlice { + t.Helper() + records := plog.NewLogRecordSlice() + + logRecords := []struct { + Body string + Attributes map[string]any + }{ + { + Body: "timestamp=2023-11-16T10:00:55.995Z kind=event event_name=faro.performanceEntry event_domain=browser event_data_name=https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css.map sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false browser_userAgent=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.12.1 Safari/537.36\" browser_language=en-US browser_viewportWidth=1920 browser_viewportHeight=1080 browser_brand_0_brand=brand1 browser_brand_0_version=1.0.0", + Attributes: map[string]any{ + "kind": "event", + }, + }, + } + + for _, logRecord := range logRecords { + lr := records.AppendEmpty() + lr.Body().SetStr(logRecord.Body) + err := lr.Attributes().FromRaw(logRecord.Attributes) + require.NoError(t, err) + } + + return records +} + +func generateLogRecordsWithBrowserBrandsAsString(t *testing.T) plog.LogRecordSlice { + t.Helper() + records := plog.NewLogRecordSlice() + + logRecords := []struct { + Body string + Attributes map[string]any + }{ + { + Body: "timestamp=2023-11-16T10:00:55.995Z kind=event event_name=faro.performanceEntry event_domain=browser event_data_name=https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css.map sdk_name=grafana-frontend-agent sdk_version=1.3.5 app_name=testapp app_namespace=testnamespace app_release=0.8.2 app_version=abcdefg app_environment=production browser_name=chrome browser_version=88.12.1 browser_os=linux browser_mobile=false browser_userAgent=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.12.1 Safari/537.36\" browser_language=en-US browser_viewportWidth=1920 browser_viewportHeight=1080 browser_brands=\"Chromium;Google Inc.;\"", + Attributes: map[string]any{ + "kind": "event", + }, + }, + } + + for _, logRecord := range logRecords { + lr := records.AppendEmpty() + lr.Body().SetStr(logRecord.Body) + err := lr.Attributes().FromRaw(logRecord.Attributes) + require.NoError(t, err) + } + + return records +} diff --git a/pkg/translator/faro/faro_to_traces.go b/pkg/translator/faro/faro_to_traces.go new file mode 100644 index 0000000..8b5bfd5 --- /dev/null +++ b/pkg/translator/faro/faro_to_traces.go @@ -0,0 +1,39 @@ +package faro + +import ( + "context" + + faroTypes "github.com/grafana/faro/pkg/go" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/otel" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +// TranslateToTraces converts faro.Payload into Traces pipeline data +func TranslateToTraces(ctx context.Context, payload faroTypes.Payload) (*ptrace.Traces, error) { + _, span := otel.Tracer("").Start(ctx, "TranslateToTraces") + defer span.End() + + if payload.Traces == nil { + return nil, nil + } + + resspanCount := payload.Traces.Traces.ResourceSpans().Len() + + traces := ptrace.NewTraces() + traces.ResourceSpans().EnsureCapacity(resspanCount) + for i := 0; i < resspanCount; i++ { + rs := traces.ResourceSpans().AppendEmpty() + frs := payload.Traces.Traces.ResourceSpans().At(i) + frs.Resource().Attributes().PutStr(string(semconv.ServiceNameKey), payload.Meta.App.Name) + frs.Resource().Attributes().PutStr(string(semconv.ServiceVersionKey), payload.Meta.App.Version) + frs.Resource().Attributes().PutStr(string(semconv.DeploymentEnvironmentKey), payload.Meta.App.Environment) + + if payload.Meta.App.Namespace != "" { + frs.Resource().Attributes().PutStr(string(semconv.ServiceNamespaceKey), payload.Meta.App.Namespace) + } + frs.CopyTo(rs) + } + + return &traces, nil +} diff --git a/pkg/translator/faro/faro_to_traces_test.go b/pkg/translator/faro/faro_to_traces_test.go new file mode 100644 index 0000000..76b47c6 --- /dev/null +++ b/pkg/translator/faro/faro_to_traces_test.go @@ -0,0 +1,147 @@ +package faro + +import ( + "context" + "encoding/hex" + "testing" + + faroTypes "github.com/grafana/faro/pkg/go" + "github.com/grafana/faro/pkg/translator/faro/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +func TestTranslateToTraces(t *testing.T) { + testcases := []struct { + name string + faroPayload faroTypes.Payload + expectedTraces *ptrace.Traces + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Standard payload", + faroPayload: *internal.PayloadFromFile(t, "payload.json"), + expectedTraces: generateTraces(t), + wantErr: assert.NoError, + }, + { + name: "Empty payload", + faroPayload: faroTypes.Payload{}, + expectedTraces: nil, + wantErr: assert.NoError, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + traces, err := TranslateToTraces(context.TODO(), tt.faroPayload) + if !tt.wantErr(t, err) { + return + } + if tt.expectedTraces == nil && assert.Nil(t, traces) { + return + } + assert.NoError(t, ptracetest.CompareTraces(*tt.expectedTraces, *traces)) + }) + } +} + +func generateTraces(t *testing.T) *ptrace.Traces { + t.Helper() + traces := ptrace.NewTraces() + resourceTraces := traces.ResourceSpans().AppendEmpty() + _ = resourceTraces.Resource().Attributes().FromRaw(map[string]any{ + string(semconv.ServiceNameKey): "testapp", + string(semconv.ServiceVersionKey): "abcdefg", + string(semconv.ServiceNamespaceKey): "testnamespace", + string(semconv.DeploymentEnvironmentKey): "production", + "telemetry.sdk.language": "webjs", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.21.0", + }) + + scopeSpans := resourceTraces.ScopeSpans().AppendEmpty() + scope := scopeSpans.Scope() + scope.SetName("@opentelemetry/instrumentation-fetch") + scope.SetVersion("0.48.0") + + span := scopeSpans.Spans().AppendEmpty() + span.SetName("HTTP GET") + + var spanID [8]byte + _, err := hex.Decode(spanID[:], []byte("f71b2cc42962650f")) + require.NoError(t, err) + span.SetSpanID(spanID) + + var traceID [16]byte + _, err = hex.Decode(traceID[:], []byte("bac44bf5d6d040fc5fdf9bc22442c6f2")) + require.NoError(t, err) + span.SetTraceID(traceID) + + span.SetKind(3) + span.SetStartTimestamp(pcommon.Timestamp(1718700770771000000)) + span.SetEndTimestamp(pcommon.Timestamp(1718700770800000000)) + err = span.Attributes().FromRaw(map[string]any{ + "component": "fetch", + "http.method": "GET", + "http.url": "http://localhost:5173/", + "session_id": "cD3am6QTPa", + "http.status_code": 200, + "http.status_text": "OK", + "http.host": "localhost:5173", + "http.scheme": "http", + "http.user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0", + "http.response_content_length": 2819, + }) + require.NoError(t, err) + events := []struct { + Name string + Timestamp uint64 + }{ + { + Name: "fetchStart", + Timestamp: 1718700770771000000, + }, + { + Name: "domainLookupStart", + Timestamp: 1718700770775000000, + }, + { + Name: "domainLookupEnd", + Timestamp: 1718700770775000000, + }, + { + Name: "connectStart", + Timestamp: 1718700770775000000, + }, + { + Name: "connectEnd", + Timestamp: 1718700770775000000, + }, + { + Name: "requestStart", + Timestamp: 1718700770775000000, + }, + { + Name: "responseStart", + Timestamp: 1718700770797000000, + }, + { + Name: "responseEnd", + Timestamp: 1718700770797000000, + }, + } + + evts := span.Events() + for _, event := range events { + evt := evts.AppendEmpty() + evt.SetName(event.Name) + evt.SetTimestamp(pcommon.Timestamp(event.Timestamp)) + } + evts.CopyTo(span.Events()) + return &traces +} diff --git a/pkg/translator/faro/go.mod b/pkg/translator/faro/go.mod index aba59c4..1279288 100644 --- a/pkg/translator/faro/go.mod +++ b/pkg/translator/faro/go.mod @@ -4,33 +4,39 @@ go 1.23.3 require ( github.com/go-logfmt/logfmt v0.6.0 - github.com/grafana/faro/pkg/go v0.0.0-20241209115835-ae0605ce4d8d + github.com/grafana/faro/pkg/go v0.0.0-20241218101758-83ff049fac1b + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.115.0 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/collector/pdata v1.21.0 - go.opentelemetry.io/otel v1.32.0 + github.com/wk8/go-ordered-map v1.0.0 + github.com/zeebo/xxh3 v1.0.2 + go.opentelemetry.io/collector/pdata v1.22.0 + go.opentelemetry.io/otel v1.33.0 ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.115.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.32.0 // indirect - go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.32.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect + google.golang.org/grpc v1.69.0 // indirect + google.golang.org/protobuf v1.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/translator/faro/go.sum b/pkg/translator/faro/go.sum index 552b6a3..ae077ae 100644 --- a/pkg/translator/faro/go.sum +++ b/pkg/translator/faro/go.sum @@ -2,7 +2,8 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMz github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,11 +25,17 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/faro/pkg/go v0.0.0-20241209115835-ae0605ce4d8d h1:mvEP8F2YujhIpQtpNLPuIUyJ38o9vmmpmd6bzC+mTTw= github.com/grafana/faro/pkg/go v0.0.0-20241209115835-ae0605ce4d8d/go.mod h1:fJa5cS+fHDPKao/KaCRgIGplg2/2UXSP8k+evjqBcqY= +github.com/grafana/faro/pkg/go v0.0.0-20241218101758-83ff049fac1b h1:iZevbV1FEXWEQOT76uiErjXnbG6jocFj8cPcW0Y2H7E= +github.com/grafana/faro/pkg/go v0.0.0-20241218101758-83ff049fac1b/go.mod h1:4CP5sfwz5+uVfNgCeA5/N6csXnzOgnkgT1jh57ASaK4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -40,6 +47,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.115.0 h1:Z9p78zj9Qblw472mGkPieuX7mqduAp47rzMbFfq5evI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.115.0/go.mod h1:mtxUxJEIQy27MaGR1yzcn/OK8NoddEgb7fumpEbKYss= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.115.0 h1:MerLKMrkM4YoGF6Di0D9yMXO02yCX8mrZAi/+jJVVeI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.115.0/go.mod h1:R8AkVWe9G5Q0oMOapvm9HNS076E3Min8SVlmhBL3QD0= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.115.0 h1:WEqcnWSy9dNSlGb8pYRBX7zhaz2ReyaeImlenbzNTB4= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.115.0/go.mod h1:6Mk71CakHUA3I6oM9hARDiyQypYyOolvb+4PFYyVEFg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -47,18 +60,35 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8= +github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector/pdata v1.21.0 h1:PG+UbiFMJ35X/WcAR7Rf/PWmWtRdW0aHlOidsR6c5MA= go.opentelemetry.io/collector/pdata v1.21.0/go.mod h1:GKb1/zocKJMvxKbS+sl0W85lxhYBTFJ6h6I1tphVyDU= +go.opentelemetry.io/collector/pdata v1.22.0 h1:3yhjL46NLdTMoP8rkkcE9B0pzjf2973crn0KKhX5UrI= +go.opentelemetry.io/collector/pdata v1.22.0/go.mod h1:nLLf6uDg8Kn5g3WNZwGyu8+kf77SwOqQvMTb5AXEbEY= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -80,6 +110,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -96,12 +127,19 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI= +google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/translator/faro/internal/keyval.go b/pkg/translator/faro/internal/keyval.go new file mode 100644 index 0000000..8fd813d --- /dev/null +++ b/pkg/translator/faro/internal/keyval.go @@ -0,0 +1,87 @@ +package internal + +import ( + "fmt" + "sort" + + om "github.com/wk8/go-ordered-map" +) + +// KeyVal is an ordered map of string to interface +type KeyVal = om.OrderedMap + +// NewKeyVal creates new empty KeyVal +func NewKeyVal() *KeyVal { + return om.New() +} + +// KeyValFromMap will instantiate KeyVal from a map[string]string +func KeyValFromMap(m map[string]string) *KeyVal { + kv := NewKeyVal() + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + KeyValAdd(kv, k, m[k]) + } + return kv +} + +// KeyValFromMap will instantiate KeyVal from a map[string]float64 +func KeyValFromFloatMap(m map[string]float64) *KeyVal { + kv := NewKeyVal() + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + kv.Set(k, m[k]) + } + return kv +} + +// MergeKeyVal will merge source in target +func MergeKeyVal(target *KeyVal, source *KeyVal) { + for el := source.Oldest(); el != nil; el = el.Next() { + target.Set(el.Key, el.Value) + } +} + +// MergeKeyValWithPrefix will merge source in target, adding a prefix to each key being merged in +func MergeKeyValWithPrefix(target *KeyVal, source *KeyVal, prefix string) { + for el := source.Oldest(); el != nil; el = el.Next() { + target.Set(fmt.Sprintf("%s%s", prefix, el.Key), el.Value) + } +} + +// KeyValAdd adds a key + value string pair to kv +func KeyValAdd(kv *KeyVal, key string, value string) { + if len(value) > 0 { + kv.Set(key, value) + } +} + +// KeyValToInterfaceSlice converts KeyVal to []interface{}, typically used for logging +func KeyValToInterfaceSlice(kv *KeyVal) []interface{} { + slice := make([]interface{}, kv.Len()*2) + idx := 0 + for el := kv.Oldest(); el != nil; el = el.Next() { + slice[idx] = el.Key + idx++ + slice[idx] = el.Value + idx++ + } + return slice +} + +// KeyValToInterfaceMap converts KeyVal to map[string]interface +func KeyValToInterfaceMap(kv *KeyVal) map[string]interface{} { + retv := make(map[string]interface{}) + for el := kv.Oldest(); el != nil; el = el.Next() { + retv[fmt.Sprint(el.Key)] = el.Value + } + return retv +} diff --git a/pkg/translator/faro/internal/payload.go b/pkg/translator/faro/internal/payload.go new file mode 100644 index 0000000..d39103a --- /dev/null +++ b/pkg/translator/faro/internal/payload.go @@ -0,0 +1,251 @@ +package internal + +import ( + "fmt" + "sort" + "strconv" + "strings" + + faroTypes "github.com/grafana/faro/pkg/go" +) + +// LogToKeyVal represents a Log object as KeyVal +func LogToKeyVal(l faroTypes.Log) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "timestamp", l.Timestamp.Format(string(faroTypes.TimeFormatRFC3339Milli))) + KeyValAdd(kv, "kind", string(faroTypes.KindLog)) + KeyValAdd(kv, "message", l.Message) + KeyValAdd(kv, "level", string(l.LogLevel)) + MergeKeyValWithPrefix(kv, KeyValFromMap(l.Context), "context_") + MergeKeyVal(kv, TraceToKeyVal(l.Trace)) + return kv +} + +// ExceptionToKeyVal represents an Exception object as KeyVal +func ExceptionToKeyVal(e faroTypes.Exception) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "timestamp", e.Timestamp.Format(string(faroTypes.TimeFormatRFC3339Milli))) + KeyValAdd(kv, "kind", string(faroTypes.KindException)) + KeyValAdd(kv, "type", e.Type) + KeyValAdd(kv, "value", e.Value) + KeyValAdd(kv, "stacktrace", ExceptionToString(e)) + MergeKeyVal(kv, TraceToKeyVal(e.Trace)) + MergeKeyValWithPrefix(kv, KeyValFromMap(e.Context), "context_") + return kv +} + +// ExceptionMessage string is concatenating of the Exception.Type and Exception.Value +func ExceptionMessage(e faroTypes.Exception) string { + return fmt.Sprintf("%s: %s", e.Type, e.Value) +} + +// ExceptionToString is the string representation of an Exception +func ExceptionToString(e faroTypes.Exception) string { + var stacktrace = ExceptionMessage(e) + if e.Stacktrace != nil { + for _, frame := range e.Stacktrace.Frames { + stacktrace += FrameToString(frame) + } + } + return stacktrace +} + +// FrameToString function converts a Frame into a human readable string +func FrameToString(frame faroTypes.Frame) string { + module := "" + if len(frame.Module) > 0 { + module = frame.Module + "|" + } + return fmt.Sprintf("\n at %s (%s%s:%v:%v)", frame.Function, module, frame.Filename, frame.Lineno, frame.Colno) +} + +// MeasurementToKeyVal representation of the measurement object +func MeasurementToKeyVal(m faroTypes.Measurement) *KeyVal { + kv := NewKeyVal() + + KeyValAdd(kv, "timestamp", m.Timestamp.Format(string(faroTypes.TimeFormatRFC3339Milli))) + KeyValAdd(kv, "kind", string(faroTypes.KindMeasurement)) + KeyValAdd(kv, "type", m.Type) + MergeKeyValWithPrefix(kv, KeyValFromMap(m.Context), "context_") + + keys := make([]string, 0, len(m.Values)) + for k := range m.Values { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + KeyValAdd(kv, k, fmt.Sprintf("%f", m.Values[k])) + } + MergeKeyVal(kv, TraceToKeyVal(m.Trace)) + + values := make(map[string]float64, len(m.Values)) + for key, value := range m.Values { + values[key] = value + } + + MergeKeyValWithPrefix(kv, KeyValFromFloatMap(values), "value_") + + return kv +} + +// EventToKeyVal produces key -> value representation of Event metadata +func EventToKeyVal(e faroTypes.Event) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "timestamp", e.Timestamp.Format(string(faroTypes.TimeFormatRFC3339Milli))) + KeyValAdd(kv, "kind", string(faroTypes.KindEvent)) + KeyValAdd(kv, "event_name", e.Name) + KeyValAdd(kv, "event_domain", e.Domain) + if e.Attributes != nil { + MergeKeyValWithPrefix(kv, KeyValFromMap(e.Attributes), "event_data_") + } + MergeKeyVal(kv, TraceToKeyVal(e.Trace)) + return kv +} + +// MetaToKeyVal produces key->value representation of the metadata +func MetaToKeyVal(m faroTypes.Meta) *KeyVal { + kv := NewKeyVal() + MergeKeyValWithPrefix(kv, SDKToKeyVal(m.SDK), "sdk_") + MergeKeyValWithPrefix(kv, AppToKeyVal(m.App), "app_") + MergeKeyValWithPrefix(kv, UserToKeyVal(m.User), "user_") + MergeKeyValWithPrefix(kv, SessionToKeyVal(m.Session), "session_") + MergeKeyValWithPrefix(kv, PageToKeyVal(m.Page), "page_") + MergeKeyValWithPrefix(kv, BrowserToKeyVal(m.Browser), "browser_") + MergeKeyValWithPrefix(kv, K6ToKeyVal(m.K6), "k6_") + MergeKeyValWithPrefix(kv, ViewToKeyVal(m.View), "view_") + MergeKeyValWithPrefix(kv, GeoToKeyVal(m.Geo), "geo_") + return kv +} + +// SDKToKeyVal produces key->value representation of Sdk metadata +func SDKToKeyVal(sdk faroTypes.SDK) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "name", sdk.Name) + KeyValAdd(kv, "version", sdk.Version) + + if len(sdk.Integrations) > 0 { + integrations := make([]string, len(sdk.Integrations)) + + for i, integration := range sdk.Integrations { + integrations[i] = SDKIntegrationToString(integration) + } + + KeyValAdd(kv, "integrations", strings.Join(integrations, ",")) + } + + return kv +} + +// SDKIntegrationToString is the string representation of an SDKIntegration +func SDKIntegrationToString(i faroTypes.SDKIntegration) string { + return fmt.Sprintf("%s:%s", i.Name, i.Version) +} + +// AppToKeyVal produces key-> value representation of App metadata +func AppToKeyVal(a faroTypes.App) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "name", a.Name) + KeyValAdd(kv, "namespace", a.Namespace) + KeyValAdd(kv, "release", a.Release) + KeyValAdd(kv, "version", a.Version) + KeyValAdd(kv, "environment", a.Environment) + return kv +} + +// UserToKeyVal produces a key->value representation User metadata +func UserToKeyVal(u faroTypes.User) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "email", u.Email) + KeyValAdd(kv, "id", u.ID) + KeyValAdd(kv, "username", u.Username) + MergeKeyValWithPrefix(kv, KeyValFromMap(u.Attributes), "attr_") + return kv +} + +// SessionToKeyVal produces key->value representation of the Session metadata +func SessionToKeyVal(s faroTypes.Session) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "id", s.ID) + MergeKeyValWithPrefix(kv, KeyValFromMap(s.Attributes), "attr_") + return kv +} + +// PageToKeyVal produces key->val representation of Page metadata +func PageToKeyVal(p faroTypes.Page) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "id", p.ID) + KeyValAdd(kv, "url", p.URL) + MergeKeyValWithPrefix(kv, KeyValFromMap(p.Attributes), "attr_") + + return kv +} + +// BrowserToKeyVal produces key->value representation of the Browser metadata +func BrowserToKeyVal(b faroTypes.Browser) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "name", b.Name) + KeyValAdd(kv, "version", b.Version) + KeyValAdd(kv, "os", b.OS) + KeyValAdd(kv, "mobile", fmt.Sprintf("%v", b.Mobile)) + KeyValAdd(kv, "userAgent", b.UserAgent) + KeyValAdd(kv, "language", b.Language) + KeyValAdd(kv, "viewportWidth", b.ViewportWidth) + KeyValAdd(kv, "viewportHeight", b.ViewportHeight) + + if brandsArray, err := b.Brands.AsBrandsArray(); err == nil { + for i, brand := range brandsArray { + MergeKeyValWithPrefix(kv, BrandToKeyVal(brand), fmt.Sprintf("brand_%d_", i)) + } + return kv + } + + if brandsString, err := b.Brands.AsBrandsString(); err == nil { + KeyValAdd(kv, "brands", brandsString) + return kv + } + + return kv +} + +func BrandToKeyVal(b faroTypes.Brand) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "brand", b.Brand) + KeyValAdd(kv, "version", b.Version) + return kv +} + +// K6ToKeyVal produces a key->value representation K6 metadata +func K6ToKeyVal(k faroTypes.K6) *KeyVal { + kv := NewKeyVal() + if k.IsK6Browser { + KeyValAdd(kv, "isK6Browser", strconv.FormatBool(k.IsK6Browser)) + } + return kv +} + +// ViewToKeyVal produces a key->value representation View metadata +func ViewToKeyVal(v faroTypes.View) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "name", v.Name) + return kv +} + +// GeoToKeyVal produces a key->value representation Geo metadata +func GeoToKeyVal(g faroTypes.Geo) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "continent_iso", g.ContinentISOCode) + KeyValAdd(kv, "country_iso", g.CountryISOCode) + KeyValAdd(kv, "subdivision_iso", g.SubdivisionISO) + KeyValAdd(kv, "city", g.City) + KeyValAdd(kv, "connection_type", g.ConnectionType) + KeyValAdd(kv, "carrier", g.Carrier) + return kv +} + +// TraceToKeyVal produces a key->value representation of the trace context object +func TraceToKeyVal(tc faroTypes.TraceContext) *KeyVal { + kv := NewKeyVal() + KeyValAdd(kv, "traceID", tc.TraceID) + KeyValAdd(kv, "spanID", tc.SpanID) + return kv +} diff --git a/pkg/translator/faro/testdata/payload-browser-brand-slice.json b/pkg/translator/faro/testdata/payload-browser-brand-slice.json new file mode 100644 index 0000000..b9fed16 --- /dev/null +++ b/pkg/translator/faro/testdata/payload-browser-brand-slice.json @@ -0,0 +1,43 @@ +{ + "events": [ + { + "name": "faro.performanceEntry", + "domain": "browser", + "attributes": { + "name": "https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css.map" + }, + "timestamp": "2023-11-16T10:00:55.995Z" + } + ], + "logs": [], + "exceptions": [], + "measurements": [], + "meta": { + "sdk": { + "name": "grafana-frontend-agent", + "version": "1.3.5" + }, + "app": { + "name": "testapp", + "namespace": "testnamespace", + "release": "0.8.2", + "version": "abcdefg", + "environment": "production", + "bundleId": "testBundleId" + }, + "browser": { + "name": "chrome", + "version": "88.12.1", + "os": "linux", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.12.1 Safari/537.36", + "mobile": false, + "language": "en-US", + "brands": [{ + "brand": "brand1", + "version": "1.0.0" + }], + "viewportWidth": "1920", + "viewportHeight": "1080" + } + } +} diff --git a/pkg/translator/faro/testdata/payload-browser-brand-string.json b/pkg/translator/faro/testdata/payload-browser-brand-string.json new file mode 100644 index 0000000..e4107ee --- /dev/null +++ b/pkg/translator/faro/testdata/payload-browser-brand-string.json @@ -0,0 +1,40 @@ +{ + "events": [ + { + "name": "faro.performanceEntry", + "domain": "browser", + "attributes": { + "name": "https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css.map" + }, + "timestamp": "2023-11-16T10:00:55.995Z" + } + ], + "logs": [], + "exceptions": [], + "measurements": [], + "meta": { + "sdk": { + "name": "grafana-frontend-agent", + "version": "1.3.5" + }, + "app": { + "name": "testapp", + "namespace": "testnamespace", + "release": "0.8.2", + "version": "abcdefg", + "environment": "production", + "bundleId": "testBundleId" + }, + "browser": { + "name": "chrome", + "version": "88.12.1", + "os": "linux", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.12.1 Safari/537.36", + "mobile": false, + "language": "en-US", + "brands": "Chromium;Google Inc.;", + "viewportWidth": "1920", + "viewportHeight": "1080" + } + } +} diff --git a/pkg/translator/faro/testdata/payload.json b/pkg/translator/faro/testdata/payload.json new file mode 100644 index 0000000..c8818d9 --- /dev/null +++ b/pkg/translator/faro/testdata/payload.json @@ -0,0 +1,488 @@ +{ + "events": [ + { + "name": "faro.performanceEntry", + "domain": "browser", + "attributes": { + "name": "https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css.map", + "entryType": "resource", + "startTime": "331", + "duration": "3371", + "initiatorType": "other", + "nextHopProtocol": "h2", + "workerStart": "0", + "redirectStart": "0", + "redirectEnd": "0", + "fetchStart": "331", + "domainLookupStart": "588", + "domainLookupEnd": "590", + "connectStart": "337", + "connectEnd": "3656", + "secureConnectionStart": "3638", + "requestStart": "3656", + "responseStart": "3690", + "responseEnd": "3702", + "transferSize": "0", + "encodedBodySize": "0", + "decodedBodySize": "0", + "serverTiming": "[]" + }, + "timestamp": "2023-11-16T10:00:55.995Z" + } + ], + "logs": [ + { + "message": "opened pricing page", + "level": "info", + "context": { + "component": "AppRoot", + "page": "Pricing" + }, + "timestamp": "2021-09-30T10:46:17.680Z", + "trace": { + "trace_id": "abcd", + "span_id": "def" + } + }, + { + "message": "loading price list", + "level": "trace", + "context": { + "component": "AppRoot", + "page": "Pricing" + }, + "timestamp": "2021-09-30T10:46:17.680Z", + "trace": { + "trace_id": "abcd", + "span_id": "ghj" + } + } + ], + "exceptions": [ + { + "type": "Error", + "value": "Cannot read property 'find' of undefined", + "stacktrace": { + "frames": [ + { + "colno": 42, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "?", + "in_app": true, + "lineno": 8639 + }, + { + "colno": 9, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "dispatchAction", + "in_app": true, + "lineno": 268095 + }, + { + "colno": 13, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "scheduleUpdateOnFiber", + "in_app": true, + "lineno": 273726 + }, + { + "colno": 7, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "flushSyncCallbackQueue", + "in_app": true, + "lineno": 263362 + }, + { + "colno": 13, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "flushSyncCallbackQueueImpl", + "in_app": true, + "lineno": 263374 + }, + { + "colno": 14, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "runWithPriority$1", + "lineno": 263325 + }, + { + "colno": 16, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "unstable_runWithPriority", + "lineno": 291265 + }, + { + "colno": 30, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "?", + "lineno": 263379 + }, + { + "colno": 22, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "performSyncWorkOnRoot", + "lineno": 274126 + }, + { + "colno": 11, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "renderRootSync", + "lineno": 274509 + }, + { + "colno": 9, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "workLoopSync", + "lineno": 274543 + }, + { + "colno": 16, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "performUnitOfWork", + "lineno": 274606 + }, + { + "colno": 18, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "beginWork$1", + "in_app": true, + "lineno": 275746 + }, + { + "colno": 20, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "beginWork", + "lineno": 270944 + }, + { + "colno": 24, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "updateFunctionComponent", + "lineno": 269291 + }, + { + "colno": 22, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "renderWithHooks", + "lineno": 266969 + }, + { + "colno": 74, + "filename": "http://fe:3002/static/js/main.chunk.js", + "function": "?", + "in_app": true, + "lineno": 2600 + }, + { + "colno": 65, + "filename": "http://fe:3002/static/js/main.chunk.js", + "function": "useGetBooksQuery", + "lineno": 1299 + }, + { + "colno": 85, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "Module.useQuery", + "lineno": 8495 + }, + { + "colno": 83, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "useBaseQuery", + "in_app": true, + "lineno": 8656 + }, + { + "colno": 14, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "useDeepMemo", + "lineno": 8696 + }, + { + "colno": 55, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "?", + "lineno": 8657 + }, + { + "colno": 47, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "QueryData.execute", + "in_app": true, + "lineno": 7883 + }, + { + "colno": 23, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "QueryData.getExecuteResult", + "lineno": 7944 + }, + { + "colno": 19, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "QueryData._this.getQueryResult", + "lineno": 7790 + }, + { + "colno": 24, + "filename": "http://fe:3002/static/js/vendors~main.chunk.js", + "function": "new ApolloError", + "in_app": true, + "lineno": 5164 + } + ] + }, + "timestamp": "2021-09-30T10:46:17.680Z", + "trace": { + "trace_id": "abcd", + "span_id": "def" + }, + "context": { + "component": "ReactErrorBoundary", + "ReactError": "Annoying Error" + } + } + ], + "measurements": [ + { + "values": { + "ttfp": 20.12, + "ttfcp": 22.12, + "ttfb": 14 + }, + "type": "page load", + "timestamp": "2021-09-30T10:46:17.680Z", + "trace": { + "trace_id": "abcd", + "span_id": "def" + }, + "context": { + "hello": "world" + } + } + ], + "meta": { + "sdk": { + "name": "grafana-frontend-agent", + "version": "1.3.5" + }, + "app": { + "name": "testapp", + "namespace": "testnamespace", + "release": "0.8.2", + "version": "abcdefg", + "environment": "production", + "bundleId": "testBundleId" + }, + "user": { + "username": "testuser", + "id": "123", + "email": "geralt@kaermorhen.org", + "attributes": { + "foo": "bar" + } + }, + "session": { + "id": "abcd", + "attributes": { + "time_elapsed": "100s" + } + }, + "page": { + "url": "https://example.com/page" + }, + "browser": { + "name": "chrome", + "version": "88.12.1", + "os": "linux", + "mobile": false + }, + "view": { + "name": "foobar" + } + }, + "traces": { + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "@grafana/faro-demo-client" + } + }, + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "webjs" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.21.0" + } + }, + { + "key": "service.version", + "value": { + "stringValue": "1.0.0" + } + }, + { + "key": "deployment.environment", + "value": { + "stringValue": "development" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "@opentelemetry/instrumentation-fetch", + "version": "0.48.0" + }, + "spans": [ + { + "traceId": "bac44bf5d6d040fc5fdf9bc22442c6f2", + "spanId": "f71b2cc42962650f", + "name": "HTTP GET", + "kind": 3, + "startTimeUnixNano": "1718700770771000000", + "endTimeUnixNano": "1718700770800000000", + "attributes": [ + { + "key": "component", + "value": { + "stringValue": "fetch" + } + }, + { + "key": "http.method", + "value": { + "stringValue": "GET" + } + }, + { + "key": "http.url", + "value": { + "stringValue": "http://localhost:5173/" + } + }, + { + "key": "session_id", + "value": { + "stringValue": "cD3am6QTPa" + } + }, + { + "key": "http.status_code", + "value": { + "intValue": 200 + } + }, + { + "key": "http.status_text", + "value": { + "stringValue": "OK" + } + }, + { + "key": "http.host", + "value": { + "stringValue": "localhost:5173" + } + }, + { + "key": "http.scheme", + "value": { + "stringValue": "http" + } + }, + { + "key": "http.user_agent", + "value": { + "stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0" + } + }, + { + "key": "http.response_content_length", + "value": { + "intValue": 2819 + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [], + "name": "fetchStart", + "timeUnixNano": "1718700770771000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domainLookupStart", + "timeUnixNano": "1718700770775000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domainLookupEnd", + "timeUnixNano": "1718700770775000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "connectStart", + "timeUnixNano": "1718700770775000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "connectEnd", + "timeUnixNano": "1718700770775000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "requestStart", + "timeUnixNano": "1718700770775000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "responseStart", + "timeUnixNano": "1718700770797000000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "responseEnd", + "timeUnixNano": "1718700770797000000", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0 + } + ] + } + ] + } + ] + } +}