From c2d9ae33c8d3a84d1b0e76161dceeae4a51eda0d Mon Sep 17 00:00:00 2001 From: Jared Harper <129781402+swi-jared@users.noreply.github.com> Date: Mon, 22 Jul 2024 14:18:38 -0700 Subject: [PATCH] [NH-78291] Lambda support (#106) Co-authored-by: tammy-baylis-swi --- examples/grpc/go.mod | 8 +- examples/grpc/go.sum | 4 + examples/http/go.mod | 8 +- examples/http/go.sum | 4 + go.mod | 32 +-- go.sum | 75 ++++--- .../aws/aws-lambda-go/swolambda/README.md | 51 +++++ .../aws/aws-lambda-go/swolambda/go.mod | 48 +++++ .../aws/aws-lambda-go/swolambda/go.sum | 66 ++++++ .../aws/aws-lambda-go/swolambda/swolambda.go | 101 ++++++++++ internal/config/config.go | 10 +- internal/entryspans/entryspans.go | 60 +++++- internal/entryspans/entryspans_test.go | 18 +- internal/exporter/exporter.go | 4 +- internal/metrics/metrics.go | 46 ++--- internal/metrics/metrics_test.go | 52 ++--- internal/metrics/otel.go | 77 +++++++ internal/metrics/registry.go | 20 +- internal/oboe/file_watcher.go | 132 ++++++++++++ internal/oboe/oboe.go | 173 +++++++++------- internal/oboe/settings.go | 51 ++--- internal/oboe/settings_lambda.go | 107 ++++++++++ internal/oboe/settings_lambda_test.go | 190 ++++++++++++++++++ internal/oboetestutils/oboe.go | 96 +-------- internal/processor/processor.go | 10 +- internal/processor/processor_test.go | 20 +- internal/reporter/reporter_grpc.go | 4 +- internal/reporter/reporter_grpc_test.go | 1 - internal/reporter/reporter_test.go | 10 +- internal/{utils/otel.go => txn/txn.go} | 51 ++--- .../{utils/otel_test.go => txn/txn_test.go} | 43 +++- internal/utils/utils.go | 61 ++++++ internal/utils/utils_test.go | 124 ++++++++++++ swo/agent.go | 109 +++++++++- 34 files changed, 1483 insertions(+), 383 deletions(-) create mode 100644 instrumentation/github.com/aws/aws-lambda-go/swolambda/README.md create mode 100644 instrumentation/github.com/aws/aws-lambda-go/swolambda/go.mod create mode 100644 instrumentation/github.com/aws/aws-lambda-go/swolambda/go.sum create mode 100644 instrumentation/github.com/aws/aws-lambda-go/swolambda/swolambda.go create mode 100644 internal/metrics/otel.go create mode 100644 internal/oboe/file_watcher.go create mode 100644 internal/oboe/settings_lambda.go create mode 100644 internal/oboe/settings_lambda_test.go rename internal/{utils/otel.go => txn/txn.go} (65%) rename internal/{utils/otel_test.go => txn/txn_test.go} (66%) create mode 100644 internal/utils/utils_test.go diff --git a/examples/grpc/go.mod b/examples/grpc/go.mod index c2500c9a..44771d06 100644 --- a/examples/grpc/go.mod +++ b/examples/grpc/go.mod @@ -22,7 +22,7 @@ replace github.com/solarwinds/apm-go => ../.. require ( github.com/solarwinds/apm-go v0.1.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 - go.opentelemetry.io/otel v1.25.0 + go.opentelemetry.io/otel v1.26.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 ) @@ -38,9 +38,9 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/solarwinds/apm-proto v0.0.0-20231107001908-432e697887b6 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - go.opentelemetry.io/otel/sdk v1.25.0 // indirect - go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/sdk v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect diff --git a/examples/grpc/go.sum b/examples/grpc/go.sum index 095ef44e..051ab6ac 100644 --- a/examples/grpc/go.sum +++ b/examples/grpc/go.sum @@ -44,12 +44,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= diff --git a/examples/http/go.mod b/examples/http/go.mod index bc0a007a..e01a28b3 100644 --- a/examples/http/go.mod +++ b/examples/http/go.mod @@ -24,8 +24,8 @@ require ( github.com/mattn/go-sqlite3 v1.14.18 github.com/solarwinds/apm-go v0.1.1 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 - go.opentelemetry.io/otel v1.25.0 - go.opentelemetry.io/otel/trace v1.25.0 + go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/otel/trace v1.26.0 ) require ( @@ -40,8 +40,8 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/solarwinds/apm-proto v0.0.0-20231107001908-432e697887b6 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - go.opentelemetry.io/otel/sdk v1.25.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/sdk v1.26.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect diff --git a/examples/http/go.sum b/examples/http/go.sum index 157b4c1a..f0608677 100644 --- a/examples/http/go.sum +++ b/examples/http/go.sum @@ -40,14 +40,18 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k= go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY= go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= diff --git a/go.mod b/go.mod index 26c0f3a7..2d1c4826 100644 --- a/go.mod +++ b/go.mod @@ -25,32 +25,40 @@ require ( github.com/pkg/errors v0.9.1 github.com/solarwinds/apm-proto v1.0.5 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 + go.opentelemetry.io/otel/metric v1.28.0 + go.opentelemetry.io/otel/sdk/metric v1.27.0 go.uber.org/atomic v1.11.0 - google.golang.org/grpc v1.63.2 + google.golang.org/grpc v1.64.0 gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.25.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/protobuf v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/protobuf v1.34.1 // indirect ) require ( github.com/stretchr/objx v0.5.2 // indirect - go.opentelemetry.io/otel v1.25.0 - go.opentelemetry.io/otel/sdk v1.25.0 - go.opentelemetry.io/otel/trace v1.25.0 - golang.org/x/sys v0.19.0 // indirect + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 + golang.org/x/sys v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index af644881..4a77f906 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 h1:vVOuhRyslJ6T/HteG71ZWCT github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6/go.mod h1:jimWaqLiT0sJGLh51dKCLLtExRYPtMU7MpxuCgtbkxg= github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -14,50 +16,73 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/solarwinds/apm-proto v1.0.5 h1:HH1bozLsH+j1Nqn/MKb+h3aNEmrI8OcCWZJFbVEvjr8= github.com/solarwinds/apm-proto v1.0.5/go.mod h1:CN4fCYBnxyOJlBV0CYNXLz6lzNH8SCfNqcCBbpai76c= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= -go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= -go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= -go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= -go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= -go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/instrumentation/github.com/aws/aws-lambda-go/swolambda/README.md b/instrumentation/github.com/aws/aws-lambda-go/swolambda/README.md new file mode 100644 index 00000000..d2a68da6 --- /dev/null +++ b/instrumentation/github.com/aws/aws-lambda-go/swolambda/README.md @@ -0,0 +1,51 @@ +# Instrumentation for AWS Lambda + +This package instruments the AWS Lambda `Handler` interface. + +## Usage + +### Add the `Otelcol` extension layer + +Follow the [SolarWinds Observability +documentation](https://documentation.solarwinds.com/en/success_center/observability/content/intro/services/aws-lambda-overview.htm) +to add the Otelcol extension layer. + +**Note**: Unlike other languages, Golang does not require an additional +extension, so the "Instrumentation extension" section on that page does not +apply. + +### Modify your code + +First, install the dependency: +```shell +go get -u github.com/solarwinds/apm-go/instrumentation/github.com/aws/aws-lambda-go/swolambda +``` + +Then, wrap your handler with `swolambda.WrapHandler`: + +```go +package main +import ( + "context" + "github.com/aws/aws-lambda-go/lambda" + "github.com/solarwinds/apm-go/instrumentation/github.com/aws/aws-lambda-go/swolambda" +) + +// Example incoming type +type MyEvent struct {} + +// This is an example handler, yours may have a different signature and a +// different name. It will work ass long as it adheres to what the Lambda SDK +// expects. (See "Valid handler signatures"[0]) +// [0] https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html +func ExampleHandler(ctx context.Context, event *MyEvent) (string, error) { + return "hello world", nil +} +func main() { + // We wrap our handler here and pass the result to `lambda.Start` + lambda.Start(swolambda.WrapHandler(ExampleHandler)) +} +``` + +Now that you've instrumented your code, you should be able to send requests and +see the resulting metrics and traces in SWO. \ No newline at end of file diff --git a/instrumentation/github.com/aws/aws-lambda-go/swolambda/go.mod b/instrumentation/github.com/aws/aws-lambda-go/swolambda/go.mod new file mode 100644 index 00000000..392b7cd0 --- /dev/null +++ b/instrumentation/github.com/aws/aws-lambda-go/swolambda/go.mod @@ -0,0 +1,48 @@ +// © 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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. + +module github.com/solarwinds/apm-go/instrumentation/github.com/aws/aws-lambda-go/swolambda + +go 1.21 + +require ( + github.com/aws/aws-lambda-go v1.47.0 + github.com/solarwinds/apm-go v1.1.0 + go.opentelemetry.io/otel v1.27.0 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coocood/freecache v1.2.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/solarwinds/apm-proto v0.0.0-20231107001908-432e697887b6 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/instrumentation/github.com/aws/aws-lambda-go/swolambda/go.sum b/instrumentation/github.com/aws/aws-lambda-go/swolambda/go.sum new file mode 100644 index 00000000..03a92d4f --- /dev/null +++ b/instrumentation/github.com/aws/aws-lambda-go/swolambda/go.sum @@ -0,0 +1,66 @@ +github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= +github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M= +github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= +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= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/solarwinds/apm-go v1.0.0 h1:oSF7fRCQE4yhvyZxwQqfFLtCbpqQCWTJrpJ822T/LCo= +github.com/solarwinds/apm-go v1.0.0/go.mod h1:NcXCeVxcj1O6/2UH8iPiWYY9Xd+g+/u04taW3CMw2zg= +github.com/solarwinds/apm-proto v0.0.0-20231107001908-432e697887b6 h1:Oyzwjp7RN7X8q3K4iK1B5+XmQL2ou993u9CGXOBkEpk= +github.com/solarwinds/apm-proto v0.0.0-20231107001908-432e697887b6/go.mod h1:CN4fCYBnxyOJlBV0CYNXLz6lzNH8SCfNqcCBbpai76c= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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/instrumentation/github.com/aws/aws-lambda-go/swolambda/swolambda.go b/instrumentation/github.com/aws/aws-lambda-go/swolambda/swolambda.go new file mode 100644 index 00000000..e84dad8d --- /dev/null +++ b/instrumentation/github.com/aws/aws-lambda-go/swolambda/swolambda.go @@ -0,0 +1,101 @@ +// © 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 swolambda + +import ( + "context" + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/solarwinds/apm-go/internal/config" + "github.com/solarwinds/apm-go/internal/log" + "github.com/solarwinds/apm-go/swo" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" + "os" + "sync" + "sync/atomic" +) + +var ( + flusher swo.Flusher + tracer trace.Tracer + initHandlerOnce sync.Once + warmStart atomic.Bool +) + +type wrappedHandler struct { + base lambda.Handler + fnName string + txnName string + region string +} + +var _ lambda.Handler = &wrappedHandler{} + +func (w *wrappedHandler) Invoke(ctx context.Context, payload []byte) ([]byte, error) { + // Note: We need to figure out how to determine `faas.trigger` attribute + // which is required by semconv + attrs := []attribute.KeyValue{ + attribute.String("sw.transaction", w.txnName), + semconv.FaaSColdstart(!warmStart.Swap(true)), + semconv.FaaSInvokedName(w.fnName), + semconv.FaaSInvokedProviderAWS, + semconv.FaaSInvokedRegion(w.region), + } + if lc, ok := lambdacontext.FromContext(ctx); !ok { + log.Error("could not obtain lambda context") + } else if lc != nil { + attrs = append(attrs, semconv.FaaSInvocationID(lc.AwsRequestID)) + } + ctx, span := tracer.Start(ctx, w.fnName, trace.WithSpanKind(trace.SpanKindServer), trace.WithAttributes(attrs...)) + defer func() { + span.End() + if flusher != nil { + if err := flusher.Flush(context.Background()); err != nil { + log.Error("could not flush lambda metrics", err) + } + } + }() + res, err := w.base.Invoke(ctx, payload) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + } + return res, err +} + +func WrapHandler(f interface{}) lambda.Handler { + initHandlerOnce.Do(func() { + var err error + if flusher, err = swo.StartLambda(lambdacontext.LogStreamName); err != nil { + log.Error("could not initialize SWO lambda instrumentation", err) + } + tracer = otel.GetTracerProvider().Tracer("swolambda") + }) + fnName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME") + txnName := config.GetTransactionName() + if txnName == "" { + txnName = fnName + } + return &wrappedHandler{ + base: lambda.NewHandler(f), + fnName: fnName, + txnName: txnName, + region: os.Getenv("AWS_REGION"), + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 2af19e2a..142716ff 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -342,8 +342,8 @@ const ( may be different from your setting.` ) -// hasLambdaEnv checks if the AWS Lambda env var is set. -func hasLambdaEnv() bool { +// HasLambdaEnv checks if the AWS Lambda env var is set. +func HasLambdaEnv() bool { return os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" && os.Getenv("LAMBDA_TASK_ROOT") != "" } @@ -364,12 +364,12 @@ func (c *Config) validate() error { c.Ec2MetadataTimeout = t } - if c.TransactionName != "" && !hasLambdaEnv() { + if c.TransactionName != "" && !HasLambdaEnv() { log.Info(InvalidEnv("TransactionName", c.TransactionName)) c.TransactionName = getFieldDefaultValue(c, "TransactionName") } - if !hasLambdaEnv() { + if !HasLambdaEnv() { if c.ServiceKey != "" { c.ServiceKey = ToServiceKey(c.ServiceKey) if ok := IsValidServiceKey(c.ServiceKey); !ok { @@ -915,7 +915,7 @@ func (c *Config) GetTransactionFiltering() []TransactionFilter { func (c *Config) GetTransactionName() string { c.RLock() defer c.RUnlock() - return c.TransactionName + return strings.TrimSpace(c.TransactionName) } // GetSQLSanitize returns the SQL sanitization level. diff --git a/internal/entryspans/entryspans.go b/internal/entryspans/entryspans.go index 29a0cce9..fc242d0f 100644 --- a/internal/entryspans/entryspans.go +++ b/internal/entryspans/entryspans.go @@ -17,34 +17,62 @@ package entryspans import ( "fmt" "github.com/pkg/errors" + "github.com/solarwinds/apm-go/internal/config" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "sync" ) var ( - state = &entrySpans{ - spans: make(map[trace.TraceID][]*entrySpan), - } + state = makeManagerFromEnv() - NotEntrySpan = errors.New("span is not an entry span") + NotEntrySpan = errors.New("span is not an entry span") + CannotSetTransactionName = errors.New("cannot set transaction, likely due to lambda environment") nullSpanID = trace.SpanID{} nullEntrySpan = &entrySpan{spanId: nullSpanID} ) +type manager interface { + push(tid trace.TraceID, sid trace.SpanID) + delete(tid trace.TraceID, sid trace.SpanID) error + current(tid trace.TraceID) (*entrySpan, bool) + setTransactionName(tid trace.TraceID, name string) error +} + type entrySpan struct { spanId trace.SpanID txnName string } -type entrySpans struct { +type stdManager struct { mut sync.RWMutex spans map[trace.TraceID][]*entrySpan } -func (e *entrySpans) push(tid trace.TraceID, sid trace.SpanID) { +type noopManager struct{} + +func (n noopManager) push(trace.TraceID, trace.SpanID) {} + +func (n noopManager) delete(trace.TraceID, trace.SpanID) error { + return nil +} + +func (n noopManager) current(trace.TraceID) (*entrySpan, bool) { + return nil, false +} + +func (n noopManager) setTransactionName(trace.TraceID, string) error { + return CannotSetTransactionName +} + +var ( + _ manager = &stdManager{} + _ manager = &noopManager{} +) + +func (e *stdManager) push(tid trace.TraceID, sid trace.SpanID) { e.mut.Lock() defer e.mut.Unlock() var list []*entrySpan @@ -56,14 +84,14 @@ func (e *entrySpans) push(tid trace.TraceID, sid trace.SpanID) { e.spans[tid] = list } -func (e *entrySpans) current(tid trace.TraceID) (*entrySpan, bool) { +func (e *stdManager) current(tid trace.TraceID) (*entrySpan, bool) { e.mut.Lock() defer e.mut.Unlock() a, ok := e.currentUnsafe(tid) return a, ok } -func (e *entrySpans) currentUnsafe(tid trace.TraceID) (*entrySpan, bool) { +func (e *stdManager) currentUnsafe(tid trace.TraceID) (*entrySpan, bool) { if list, ok := e.spans[tid]; ok { l := len(list) if len(list) == 0 { @@ -85,7 +113,7 @@ func Push(span sdktrace.ReadOnlySpan) error { return nil } -func (e *entrySpans) delete(tid trace.TraceID, sid trace.SpanID) error { +func (e *stdManager) delete(tid trace.TraceID, sid trace.SpanID) error { e.mut.Lock() defer e.mut.Unlock() @@ -125,7 +153,7 @@ func Current(tid trace.TraceID) (trace.SpanID, bool) { return curr.spanId, ok } -func (e *entrySpans) setTransactionName(tid trace.TraceID, name string) error { +func (e *stdManager) setTransactionName(tid trace.TraceID, name string) error { e.mut.Lock() defer e.mut.Unlock() @@ -152,3 +180,15 @@ func IsEntrySpan(span sdktrace.ReadOnlySpan) bool { parent := span.Parent() return !parent.IsValid() || parent.IsRemote() } + +func makeManagerFromEnv() manager { + if config.HasLambdaEnv() { + // In Lambda, we cannot modify the outgoing spans for transaction naming, + // thus we do not want to track entry spans. + return &noopManager{} + } else { + return &stdManager{ + spans: make(map[trace.TraceID][]*entrySpan), + } + } +} diff --git a/internal/entryspans/entryspans_test.go b/internal/entryspans/entryspans_test.go index e8823da1..97242926 100644 --- a/internal/entryspans/entryspans_test.go +++ b/internal/entryspans/entryspans_test.go @@ -33,7 +33,7 @@ var ( span4 = trace.SpanID{0x4} ) -func (e *entrySpans) pop(tid trace.TraceID) (trace.SpanID, bool) { +func (e *stdManager) pop(tid trace.TraceID) (trace.SpanID, bool) { e.mut.Lock() defer e.mut.Unlock() @@ -56,7 +56,14 @@ func (e *entrySpans) pop(tid trace.TraceID) (trace.SpanID, bool) { } } +func (e *stdManager) reset() { + e.mut.Lock() + defer e.mut.Unlock() + clear(e.spans) +} + func TestCurrent(t *testing.T) { + state := state.(*stdManager) sid, ok := Current(traceA) require.False(t, ok) require.False(t, sid.IsValid()) @@ -109,6 +116,7 @@ func TestCurrent(t *testing.T) { } func TestPush(t *testing.T) { + state := state.(*stdManager) var err error tr, teardown := testutils.TracerSetup() defer teardown() @@ -132,8 +140,8 @@ func TestPush(t *testing.T) { } func TestSetTransactionName(t *testing.T) { - // reset state - state = &entrySpans{spans: make(map[trace.TraceID][]*entrySpan)} + state := state.(*stdManager) + state.reset() err := SetTransactionName(traceA, "foo bar") require.Error(t, err) @@ -173,8 +181,8 @@ func TestSetTransactionName(t *testing.T) { } func TestDelete(t *testing.T) { - // reset state - state = &entrySpans{spans: make(map[trace.TraceID][]*entrySpan)} + state := state.(*stdManager) + state.reset() err := state.delete(traceA, span1) require.Error(t, err) diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index f950ff8d..e88d13ca 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -22,7 +22,7 @@ import ( "github.com/solarwinds/apm-go/internal/log" "github.com/solarwinds/apm-go/internal/reporter" "github.com/solarwinds/apm-go/internal/swotel/semconv" - "github.com/solarwinds/apm-go/internal/utils" + "github.com/solarwinds/apm-go/internal/txn" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -45,7 +45,7 @@ func (e *exporter) exportSpan(_ context.Context, s sdktrace.ReadOnlySpan) { attribute.String("otel.scope.version", s.InstrumentationScope().Version), }) if entryspans.IsEntrySpan(s) { - evt.AddKV(attribute.String("TransactionName", utils.GetTransactionName(s))) + evt.AddKV(attribute.String("TransactionName", txn.GetTransactionName(s))) // We MUST clear the entry span here. The SpanProcessor only clears entry spans when they are `RecordOnly` if err := entryspans.Delete(s); err != nil { log.Warningf( diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index b46b4d4a..88d6714c 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -65,13 +65,6 @@ const ( TriggeredTraceCount = "TriggeredTraceCount" ) -// Request counters collection categories -const ( - RCRegular = "ReqCounterRegular" - RCRelaxedTriggerTrace = "ReqCounterRelaxedTriggerTrace" - RCStrictTriggerTrace = "ReqCounterStrictTriggerTrace" -) - // metric names const ( transactionResponseTime = "TransactionResponseTime" @@ -186,6 +179,11 @@ func (s *EventQueueStats) TotalEventsAdd(n int64) { // RateCounts is the rate counts reported by trace sampler type RateCounts struct{ requested, sampled, limited, traced, through int64 } +// RateCountSummary is used to merge RateCounts from multiple token buckets +type RateCountSummary struct { + Requested, Traced, Limited, TtTraced, Sampled, Through int64 +} + // FlushRateCounts reset the counters and returns the current value func (c *RateCounts) FlushRateCounts() *RateCounts { return &RateCounts{ @@ -238,32 +236,16 @@ func (c *RateCounts) Through() int64 { } // addRequestCounters add various request-related counters to the metrics message buffer. -func addRequestCounters(bbuf *bson.Buffer, index *int, rcs map[string]*RateCounts) { - var requested, traced, limited, ttTraced int64 - - for _, rc := range rcs { - requested += rc.Requested() - traced += rc.Traced() - limited += rc.Limited() - } - - addMetricsValue(bbuf, index, RequestCount, requested) - addMetricsValue(bbuf, index, TraceCount, traced) - addMetricsValue(bbuf, index, TokenBucketExhaustionCount, limited) - - if rcRegular, ok := rcs[RCRegular]; ok { - addMetricsValue(bbuf, index, SampleCount, rcRegular.Sampled()) - addMetricsValue(bbuf, index, ThroughTraceCount, rcRegular.Through()) - } - - if relaxed, ok := rcs[RCRelaxedTriggerTrace]; ok { - ttTraced += relaxed.Traced() - } - if strict, ok := rcs[RCStrictTriggerTrace]; ok { - ttTraced += strict.Traced() +func addRequestCounters(bbuf *bson.Buffer, index *int, rcs *RateCountSummary) { + if rcs == nil { + return } - - addMetricsValue(bbuf, index, TriggeredTraceCount, ttTraced) + addMetricsValue(bbuf, index, RequestCount, rcs.Requested) + addMetricsValue(bbuf, index, TraceCount, rcs.Traced) + addMetricsValue(bbuf, index, TokenBucketExhaustionCount, rcs.Limited) + addMetricsValue(bbuf, index, SampleCount, rcs.Sampled) + addMetricsValue(bbuf, index, ThroughTraceCount, rcs.Through) + addMetricsValue(bbuf, index, TriggeredTraceCount, rcs.TtTraced) } // SetCap sets the maximum number of distinct metrics allowed. diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go index 5ea6b4fc..dcdff5e8 100644 --- a/internal/metrics/metrics_test.go +++ b/internal/metrics/metrics_test.go @@ -381,13 +381,17 @@ func TestAddHistogramToBSON(t *testing.T) { } func TestGenerateMetricsMessage(t *testing.T) { - reg := NewLegacyRegistry().(*registry) + reg := NewLegacyRegistry(false).(*registry) flushInterval := int32(60) bbuf := bson.WithBuf(reg.BuildBuiltinMetricsMessage(flushInterval, &EventQueueStats{}, - map[string]*RateCounts{ // requested, sampled, limited, traced, through - RCRegular: {10, 2, 5, 5, 1}, - RCRelaxedTriggerTrace: {3, 0, 1, 2, 0}, - RCStrictTriggerTrace: {4, 0, 3, 1, 0}}, true)) + &RateCountSummary{ + Requested: 10, + Sampled: 2, + Limited: 5, + Traced: 5, + Through: 1, + TtTraced: 3, + }, true)) m, err := bsonToMap(bbuf) require.NoError(t, err) @@ -407,8 +411,6 @@ func TestGenerateMetricsMessage(t *testing.T) { value interface{} } - t.Logf("Got metrics: %+v", mts) - testCases := []testCase{ {"RequestCount", int64(10)}, {"TraceCount", int64(5)}, @@ -463,11 +465,15 @@ func TestGenerateMetricsMessage(t *testing.T) { for i, tc := range testCases { assert.Equal(t, tc.name, mts[i].(map[string]interface{})["name"]) assert.IsType(t, mts[i].(map[string]interface{})["value"], tc.value, tc.name) + // test the values of the sample rate metrics + if i < 6 { + assert.Equal(t, tc.value, mts[i].(map[string]interface{})["value"], tc.name) + } } assert.Nil(t, m["TransactionNameOverflow"]) - reg = NewLegacyRegistry().(*registry) + reg = NewLegacyRegistry(false).(*registry) for i := 0; i <= metricsTransactionsMaxDefault; i++ { if !reg.apmMetrics.txnMap.isWithinLimit("Transaction-" + strconv.Itoa(i)) { break @@ -475,7 +481,7 @@ func TestGenerateMetricsMessage(t *testing.T) { } m, err = bsonToMap(bson.WithBuf(reg.BuildBuiltinMetricsMessage(flushInterval, &EventQueueStats{}, - map[string]*RateCounts{RCRegular: {}, RCRelaxedTriggerTrace: {}, RCStrictTriggerTrace: {}}, true))) + &RateCountSummary{}, true))) require.NoError(t, err) assert.NotNil(t, m["TransactionNameOverflow"]) @@ -546,9 +552,9 @@ func TestRecordSpan(t *testing.T) { ), ) span.End(trace.WithTimestamp(now.Add(1 * time.Second))) - reg := NewLegacyRegistry().(*registry) + reg := NewLegacyRegistry(false).(*registry) - reg.RecordSpan(span.(sdktrace.ReadOnlySpan), false) + reg.RecordSpan(span.(sdktrace.ReadOnlySpan)) m := reg.apmMetrics.CopyAndReset(60) assert.NotEmpty(t, m.m) @@ -567,7 +573,6 @@ func TestRecordSpan(t *testing.T) { assert.Equal(t, responseTime, v.Name) h := reg.apmHistograms.histograms - reg = NewLegacyRegistry().(*registry) assert.NotEmpty(t, h) globalHisto := h[""] granularHisto := h["my cool route"] @@ -579,8 +584,9 @@ func TestRecordSpan(t *testing.T) { assert.Equal(t, 1.001472e+06, granularHisto.hist.Mean()) assert.Equal(t, int64(1), granularHisto.hist.TotalCount()) + reg = NewLegacyRegistry(true).(*registry) // Now test for AO - reg.RecordSpan(span.(sdktrace.ReadOnlySpan), true) + reg.RecordSpan(span.(sdktrace.ReadOnlySpan)) m = reg.apmMetrics.CopyAndReset(60) assert.NotEmpty(t, m.m) @@ -637,8 +643,8 @@ func TestRecordSpanErrorStatus(t *testing.T) { ) span.End(trace.WithTimestamp(now.Add(1 * time.Second))) - reg := NewLegacyRegistry().(*registry) - reg.RecordSpan(span.(sdktrace.ReadOnlySpan), false) + reg := NewLegacyRegistry(false).(*registry) + reg.RecordSpan(span.(sdktrace.ReadOnlySpan)) m := reg.apmMetrics.CopyAndReset(60) assert.NotEmpty(t, m.m) @@ -657,7 +663,6 @@ func TestRecordSpanErrorStatus(t *testing.T) { assert.Equal(t, responseTime, v.Name) h := reg.apmHistograms.histograms - reg = NewLegacyRegistry().(*registry) assert.NotEmpty(t, h) globalHisto := h[""] granularHisto := h["my cool route"] @@ -670,7 +675,8 @@ func TestRecordSpanErrorStatus(t *testing.T) { assert.Equal(t, int64(1), granularHisto.hist.TotalCount()) // Now test for AO - reg.RecordSpan(span.(sdktrace.ReadOnlySpan), true) + reg = NewLegacyRegistry(true).(*registry) + reg.RecordSpan(span.(sdktrace.ReadOnlySpan)) m = reg.apmMetrics.CopyAndReset(60) assert.NotEmpty(t, m.m) @@ -740,14 +746,14 @@ func TestRecordSpanOverflow(t *testing.T) { ) span2.End(trace.WithTimestamp(now.Add(1 * time.Second))) - reg := NewLegacyRegistry().(*registry) + reg := NewLegacyRegistry(false).(*registry) // The cap only takes affect after the following reset reg.SetApmMetricsCap(1) reg.apmMetrics.CopyAndReset(60) assert.Equal(t, int32(1), reg.ApmMetricsCap()) - reg.RecordSpan(span.(sdktrace.ReadOnlySpan), false) - reg.RecordSpan(span2.(sdktrace.ReadOnlySpan), false) + reg.RecordSpan(span.(sdktrace.ReadOnlySpan)) + reg.RecordSpan(span2.(sdktrace.ReadOnlySpan)) m := reg.apmMetrics.CopyAndReset(60) // We expect to have a record for `my cool route` and one for `other` @@ -826,13 +832,13 @@ func TestRecordSpanOverflowAppoptics(t *testing.T) { // The cap only takes affect after the following reset // Appoptics-style will generate 3 metrics, so we'll set the cap to that here - reg := NewLegacyRegistry().(*registry) + reg := NewLegacyRegistry(true).(*registry) reg.SetApmMetricsCap(3) reg.apmMetrics.CopyAndReset(60) assert.Equal(t, int32(3), reg.ApmMetricsCap()) - reg.RecordSpan(span.(sdktrace.ReadOnlySpan), true) - reg.RecordSpan(span2.(sdktrace.ReadOnlySpan), true) + reg.RecordSpan(span.(sdktrace.ReadOnlySpan)) + reg.RecordSpan(span2.(sdktrace.ReadOnlySpan)) m := reg.apmMetrics.CopyAndReset(60) // We expect to have 3 records for `my cool route` and 3 for `other` diff --git a/internal/metrics/otel.go b/internal/metrics/otel.go new file mode 100644 index 00000000..083f5ba0 --- /dev/null +++ b/internal/metrics/otel.go @@ -0,0 +1,77 @@ +// © 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 metrics + +import ( + "context" + "github.com/solarwinds/apm-go/internal/txn" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" +) + +type otelRegistry struct { + histo metric.Int64Histogram +} + +var searchSet = map[attribute.Key]bool{ + semconv.HTTPMethodKey: true, + semconv.HTTPStatusCodeKey: true, + semconv.HTTPRouteKey: true, +} + +func (o *otelRegistry) RecordSpan(span sdktrace.ReadOnlySpan) { + var attrs = []attribute.KeyValue{ + attribute.Bool("sw.is_error", span.Status().Code == codes.Error), + attribute.String("sw.transaction", txn.GetTransactionName(span)), + } + if span.SpanKind() == trace.SpanKindServer { + for _, attr := range span.Attributes() { + if searchSet[attr.Key] { + attrs = append(attrs, attr) + } + } + } + duration := span.EndTime().Sub(span.StartTime()) + o.histo.Record( + context.Background(), + duration.Milliseconds(), + metric.WithAttributes(attrs...), + ) +} + +var _ MetricRegistry = &otelRegistry{} + +func NewOtelRegistry(p metric.MeterProvider) (MetricRegistry, error) { + meter := p.Meter("sw.apm.request.metrics") + if histo, err := meter.Int64Histogram( + "trace.service.response_time", + metric.WithExplicitBucketBoundaries(), + metric.WithUnit("ms"), + ); err != nil { + return nil, err + } else { + return &otelRegistry{histo: histo}, nil + } +} + +func TemporalitySelector(sdkmetric.InstrumentKind) metricdata.Temporality { + return metricdata.DeltaTemporality +} diff --git a/internal/metrics/registry.go b/internal/metrics/registry.go index 4d25db5d..d60c8a78 100644 --- a/internal/metrics/registry.go +++ b/internal/metrics/registry.go @@ -19,7 +19,7 @@ import ( "github.com/solarwinds/apm-go/internal/bson" "github.com/solarwinds/apm-go/internal/log" "github.com/solarwinds/apm-go/internal/swotel/semconv" - "github.com/solarwinds/apm-go/internal/utils" + "github.com/solarwinds/apm-go/internal/txn" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/sdk/trace" trace2 "go.opentelemetry.io/otel/trace" @@ -31,11 +31,12 @@ type registry struct { apmHistograms *histograms apmMetrics *measurements customMetrics *measurements + isAppoptics bool } var _ LegacyRegistry = ®istry{} -func NewLegacyRegistry() LegacyRegistry { +func NewLegacyRegistry(isAppoptics bool) LegacyRegistry { return ®istry{ apmHistograms: &histograms{ histograms: make(map[string]*histogram), @@ -43,17 +44,18 @@ func NewLegacyRegistry() LegacyRegistry { }, apmMetrics: newMeasurements(false, metricsTransactionsMaxDefault), customMetrics: newMeasurements(true, metricsCustomMetricsMaxDefault), + isAppoptics: isAppoptics, } } type MetricRegistry interface { - RecordSpan(span trace.ReadOnlySpan, isAppoptics bool) + RecordSpan(span trace.ReadOnlySpan) } type LegacyRegistry interface { MetricRegistry BuildBuiltinMetricsMessage(flushInterval int32, qs *EventQueueStats, - rcs map[string]*RateCounts, runtimeMetrics bool) []byte + rcs *RateCountSummary, runtimeMetrics bool) []byte BuildCustomMetricsMessage(flushInterval int32) []byte ApmMetricsCap() int32 SetApmMetricsCap(int32) @@ -95,7 +97,7 @@ func (r *registry) BuildCustomMetricsMessage(flushInterval int32) []byte { // // return metrics message in BSON format func (r *registry) BuildBuiltinMetricsMessage(flushInterval int32, qs *EventQueueStats, - rcs map[string]*RateCounts, runtimeMetrics bool) []byte { + rcs *RateCountSummary, runtimeMetrics bool) []byte { var m = r.apmMetrics.CopyAndReset(flushInterval) if m == nil { return nil @@ -163,7 +165,7 @@ func (r *registry) BuildBuiltinMetricsMessage(flushInterval int32, qs *EventQueu return bbuf.GetBuf() } -func (r *registry) RecordSpan(span trace.ReadOnlySpan, isAppoptics bool) { +func (r *registry) RecordSpan(span trace.ReadOnlySpan) { method := "" status := int64(0) isError := span.Status().Code == codes.Error @@ -192,7 +194,7 @@ func (r *registry) RecordSpan(span trace.ReadOnlySpan, isAppoptics bool) { } swoTags["sw.is_error"] = strconv.FormatBool(isError) - txnName := utils.GetTransactionName(span) + txnName := txn.GetTransactionName(span) swoTags["sw.transaction"] = txnName duration := span.EndTime().Sub(span.StartTime()) @@ -207,7 +209,7 @@ func (r *registry) RecordSpan(span trace.ReadOnlySpan, isAppoptics bool) { var tagsList []map[string]string var metricName string - if !isAppoptics { + if !r.isAppoptics { tagsList = []map[string]string{swoTags} metricName = responseTime } else { @@ -217,7 +219,7 @@ func (r *registry) RecordSpan(span trace.ReadOnlySpan, isAppoptics bool) { r.apmHistograms.recordHistogram("", duration) if err := s.processMeasurements(metricName, tagsList, r.apmMetrics); errors.Is(err, ErrExceedsMetricsCountLimit) { - if isAppoptics { + if r.isAppoptics { s.Transaction = OtherTransactionName tagsList = s.appOpticsTagsList() } else { diff --git a/internal/oboe/file_watcher.go b/internal/oboe/file_watcher.go new file mode 100644 index 00000000..6d78af76 --- /dev/null +++ b/internal/oboe/file_watcher.go @@ -0,0 +1,132 @@ +// © 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 oboe + +import ( + "context" + "os" + "time" + + "github.com/solarwinds/apm-go/internal/log" +) + +const ( + settingsCheckDuration = 10 * time.Second + settingsFileName = "/tmp/solarwinds-apm-settings.json" + + timeoutEnv = "SW_APM_INITIAL_SETTINGS_FILE_TIMEOUT" +) + +var exit = make(chan bool, 1) + +type FileBasedWatcher interface { + Start() + Stop() +} + +// NewFileBasedWatcher returns a FileBasedWatcher that periodically +// reads lambda settings from file +func NewFileBasedWatcher(oboe Oboe) FileBasedWatcher { + return &fileBasedWatcher{ + oboe, + } +} + +type fileBasedWatcher struct { + o Oboe +} + +// readSettingFromFile parses, normalizes, and print settings from file +func (w *fileBasedWatcher) readSettingFromFile() { + s, err := newSettingLambdaFromFile() + if os.IsNotExist(err) { + log.Debug("Settings file does not yet exist") + return + } else if err != nil { + log.Errorf("Could not read setting from file: %s", err) + return + } + log.Debugf( + "Got lambda settings from file:\n%+v", + s, + ) + w.o.UpdateSetting(s.flags, s.value, time.Second*time.Duration(s.ttl), s.args) +} + +// Start runs a ticker that checks settings expiry from cache +// and, if expired, updates cache and oboe settings. +func (w *fileBasedWatcher) Start() { + ticker := time.NewTicker(settingsCheckDuration) + waitForSettingsFile() + go func() { + defer ticker.Stop() + for { + select { + case <-exit: + return + case <-ticker.C: + w.readSettingFromFile() + } + } + }() + w.readSettingFromFile() +} + +func (w *fileBasedWatcher) Stop() { + log.Info("Stopping settings file watcher.") + exit <- true +} + +func waitForSettingsFile() { + var timeout = 1 * time.Second + if timeoutStr := os.Getenv(timeoutEnv); timeoutStr != "" { + if override, err := time.ParseDuration(timeoutStr); err != nil { + log.Errorf("could not parse duration from %s '%s': %s", timeoutEnv, timeoutStr, err) + } else if int64(override) < 1 { + log.Infof("%s was 0 or negative, skipping wait for settings file", timeoutEnv) + return + } else { + timeout = override + } + } + log.Debugf("Waiting for settings file for up to %s (override with %s; set to 0 to skip)", timeout, timeoutEnv) + // We could use something like fsnotify, but that's overkill for something this simple + waitTicker := time.NewTicker(10 * time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + defer waitTicker.Stop() + for { + select { + case <-waitTicker.C: + { + _, err := os.Stat(settingsFileName) + if err == nil { + log.Info("Settings file found") + return + } else if os.IsNotExist(err) { + log.Debug("Settings file does not yet exist") + } else { + log.Errorf("Could not read settings from file: %s", err) + return + } + } + case <-ctx.Done(): + { + log.Info("timed out waiting for settings file") + return + } + } + } +} diff --git a/internal/oboe/oboe.go b/internal/oboe/oboe.go index 8d302372..6a5849c4 100644 --- a/internal/oboe/oboe.go +++ b/internal/oboe/oboe.go @@ -15,19 +15,22 @@ package oboe import ( + "context" "encoding/binary" "errors" "fmt" + "go.opentelemetry.io/otel/metric" + "math" + "strings" + "sync/atomic" + "time" + "github.com/solarwinds/apm-go/internal/config" "github.com/solarwinds/apm-go/internal/constants" "github.com/solarwinds/apm-go/internal/log" "github.com/solarwinds/apm-go/internal/metrics" "github.com/solarwinds/apm-go/internal/rand" "github.com/solarwinds/apm-go/internal/w3cfmt" - "math" - "strings" - "sync" - "time" ) const ( @@ -46,47 +49,100 @@ const ( ) type Oboe interface { - UpdateSetting(sType int32, layer string, flags []byte, value int64, ttl int64, args map[string][]byte) + UpdateSetting(flags []byte, value int64, ttl time.Duration, args map[string][]byte) CheckSettingsTimeout() - GetSetting() (*settings, bool) + GetSetting() *settings RemoveSetting() HasDefaultSetting() bool SampleRequest(continued bool, url string, triggerTrace TriggerTraceMode, swState w3cfmt.SwTraceState) SampleDecision - FlushRateCounts() map[string]*metrics.RateCounts + FlushRateCounts() *metrics.RateCountSummary GetTriggerTraceToken() ([]byte, error) + RegisterOtelSampleRateMetrics(mp metric.MeterProvider) error } func NewOboe() Oboe { - return &oboe{ - settings: make(map[settingKey]*settings), - } + return &oboe{} } type oboe struct { - sync.RWMutex - settings map[settingKey]*settings + settings atomic.Pointer[settings] } var _ Oboe = &oboe{} +func (o *oboe) RegisterOtelSampleRateMetrics(mp metric.MeterProvider) error { + meter := mp.Meter("sw.apm.sampling.metrics") + traceCount, err := meter.Int64ObservableGauge("trace.service.tracecount") + if err != nil { + return err + } + sampleCount, err := meter.Int64ObservableGauge("trace.service.samplecount") + if err != nil { + return err + } + requestCount, err := meter.Int64ObservableGauge("trace.service.request_count") + if err != nil { + return err + } + tokenBucketExhaustionCount, err := meter.Int64ObservableGauge("trace.service.tokenbucket_exhaustion_count") + if err != nil { + return err + } + throughTraceCount, err := meter.Int64ObservableGauge("trace.service.through_trace_count") + if err != nil { + return err + } + triggeredTraceCount, err := meter.Int64ObservableGauge("trace.service.triggered_trace_count") + if err != nil { + return err + } + + _, err = meter.RegisterCallback( + func(_ context.Context, obs metric.Observer) error { + if rateCounts := o.FlushRateCounts(); rateCounts != nil { + obs.ObserveInt64(traceCount, rateCounts.Traced) + obs.ObserveInt64(sampleCount, rateCounts.Sampled) + obs.ObserveInt64(requestCount, rateCounts.Requested) + obs.ObserveInt64(tokenBucketExhaustionCount, rateCounts.Limited) + obs.ObserveInt64(throughTraceCount, rateCounts.Through) + obs.ObserveInt64(triggeredTraceCount, rateCounts.TtTraced) + } + return nil + }, + traceCount, + sampleCount, + requestCount, + tokenBucketExhaustionCount, + throughTraceCount, + triggeredTraceCount, + ) + return err +} + // FlushRateCounts collects the request counters values by categories. -func (o *oboe) FlushRateCounts() map[string]*metrics.RateCounts { - setting, ok := o.GetSetting() - if !ok { +func (o *oboe) FlushRateCounts() *metrics.RateCountSummary { + s := o.GetSetting() + if s == nil { return nil } - rcs := make(map[string]*metrics.RateCounts) - rcs[metrics.RCRegular] = setting.bucket.FlushRateCounts() - rcs[metrics.RCRelaxedTriggerTrace] = setting.triggerTraceRelaxedBucket.FlushRateCounts() - rcs[metrics.RCStrictTriggerTrace] = setting.triggerTraceStrictBucket.FlushRateCounts() - - return rcs + regular := s.bucket.FlushRateCounts() + relaxedTT := s.triggerTraceRelaxedBucket.FlushRateCounts() + strictTT := s.triggerTraceStrictBucket.FlushRateCounts() + + return &metrics.RateCountSummary{ + Sampled: regular.Sampled(), + Through: regular.Through(), + Requested: regular.Requested() + relaxedTT.Requested() + strictTT.Requested(), + Traced: regular.Traced() + relaxedTT.Traced() + strictTT.Traced(), + Limited: regular.Limited() + relaxedTT.Limited() + strictTT.Limited(), + TtTraced: relaxedTT.Traced() + strictTT.Traced(), + } } // SampleRequest returns a SampleDecision based on inputs and state of various token buckets func (o *oboe) SampleRequest(continued bool, url string, triggerTrace TriggerTraceMode, swState w3cfmt.SwTraceState) SampleDecision { - setting, ok := o.GetSetting() - if !ok { + setting := o.GetSetting() + if setting == nil { return SampleDecision{false, 0, SampleSourceNone, false, TtSettingsNotAvailable, 0, 0, false} } @@ -212,16 +268,15 @@ func adjustSampleRate(rate int64) int { return int(rate) } -func (o *oboe) UpdateSetting(sType int32, layer string, flags []byte, value int64, ttl int64, args map[string][]byte) { +func (o *oboe) UpdateSetting(flags []byte, value int64, ttl time.Duration, args map[string][]byte) { ns := newOboeSettings() ns.timestamp = time.Now() - ns.source = settingType(sType).toSampleSource() + ns.source = SampleSourceDefault ns.flags = flagStringToBin(string(flags)) ns.originalFlags = ns.flags ns.value = adjustSampleRate(value) ns.ttl = ttl - ns.layer = layer ns.TriggerToken = args[constants.KvSignatureKey] @@ -237,16 +292,9 @@ func (o *oboe) UpdateSetting(sType int32, layer string, flags []byte, value int6 tStrictCapacity := parseFloat64(args, constants.KvTriggerTraceStrictBucketCapacity, 0) ns.triggerTraceStrictBucket.setRateCap(tStrictRate, tStrictCapacity) - merged := mergeLocalSetting(ns) + ns.MergeLocalSetting() - key := settingKey{ - sType: settingType(sType), - layer: layer, - } - - o.Lock() - o.settings[key] = merged - o.Unlock() + o.settings.Store(ns) } // CheckSettingsTimeout checks and deletes expired settings @@ -255,57 +303,34 @@ func (o *oboe) CheckSettingsTimeout() { } func (o *oboe) checkSettingsTimeout() { - o.Lock() - defer o.Unlock() - - ss := o.settings - for k, s := range ss { - e := s.timestamp.Add(time.Duration(s.ttl) * time.Second) - if e.Before(time.Now()) { - delete(ss, k) - } - } -} - -func (o *oboe) GetSetting() (*settings, bool) { - o.RLock() - defer o.RUnlock() - - // always use the default setting - key := settingKey{ - sType: TypeDefault, - layer: "", + s := o.settings.Load() + if s == nil { + log.Debug("checkSettingsTimeout: No settings") + return } - if setting, ok := o.settings[key]; ok { - return setting, true + e := s.timestamp.Add(s.ttl) + log.Debugf("checkSettingsTimeout: ttl: %s, timestamp: %s, boundary: %s", s.ttl, s.timestamp, e) + if e.Before(time.Now()) { + log.Debugf("checkSettingsTimeout: ttl exceeded, expiring settings") + o.settings.Store(nil) } +} - return nil, false +func (o *oboe) GetSetting() *settings { + return o.settings.Load() } func (o *oboe) RemoveSetting() { - o.Lock() - defer o.Unlock() - - // always use the default setting - key := settingKey{ - sType: TypeDefault, - layer: "", - } - - delete(o.settings, key) + o.settings.Store(nil) } func (o *oboe) HasDefaultSetting() bool { - if _, ok := o.GetSetting(); ok { - return true - } - return false + return o.settings.Load() != nil } func (o *oboe) GetTriggerTraceToken() ([]byte, error) { - setting, ok := o.GetSetting() - if !ok { + setting := o.GetSetting() + if setting == nil { return nil, errors.New("failed to get settings") } if len(setting.TriggerToken) == 0 { diff --git a/internal/oboe/settings.go b/internal/oboe/settings.go index 9d0bfe29..22a81b0b 100644 --- a/internal/oboe/settings.go +++ b/internal/oboe/settings.go @@ -32,8 +32,7 @@ type settings struct { value int // The sample source after negotiating with local config source SampleSource - ttl int64 - layer string + ttl time.Duration TriggerToken []byte bucket *tokenBucket triggerTraceRelaxedBucket *tokenBucket @@ -43,6 +42,7 @@ type settings struct { func (s *settings) hasOverrideFlag() bool { return s.originalFlags&FlagOverride != 0 } + func newOboeSettings() *settings { return &settings{ // The global token bucket. Trace decisions of all the requests are controlled @@ -58,29 +58,26 @@ func newOboeSettings() *settings { } } -// mergeLocalSetting follow the predefined precedence to decide which one to +// MergeLocalSetting follow the predefined precedence to decide which one to // pick from: either the local configs or the remote ones, or the combination. -// -// Note: This function modifies the argument in place. -func mergeLocalSetting(remote *settings) *settings { - if remote.hasOverrideFlag() && config.SamplingConfigured() { +func (s *settings) MergeLocalSetting() { + if s.hasOverrideFlag() && config.SamplingConfigured() { // Choose the lower sample rate and merge the flags - if remote.value > config.GetSampleRate() { - remote.value = config.GetSampleRate() - remote.source = SampleSourceFile + if s.value > config.GetSampleRate() { + s.value = config.GetSampleRate() + s.source = SampleSourceFile } - remote.flags &= NewTracingMode(config.GetTracingMode()).toFlags() + s.flags &= NewTracingMode(config.GetTracingMode()).toFlags() } else if config.SamplingConfigured() { // Use local sample rate and tracing mode config - remote.value = config.GetSampleRate() - remote.flags = NewTracingMode(config.GetTracingMode()).toFlags() - remote.source = SampleSourceFile + s.value = config.GetSampleRate() + s.flags = NewTracingMode(config.GetTracingMode()).toFlags() + s.source = SampleSourceFile } if !config.GetTriggerTrace() { - remote.flags = remote.flags &^ (1 << FlagTriggerTraceOffset) + s.flags = s.flags &^ (1 << FlagTriggerTraceOffset) } - return remote } // mergeURLSetting merges the service level setting (merged from remote and local @@ -123,19 +120,8 @@ func (s *settings) getTokenBucketSetting(ttMode TriggerTraceMode) (capacity floa return bucket.capacity, bucket.ratePerSec } -// The identifying keys for a setting -type settingKey struct { - sType settingType - layer string -} -type settingType int type settingFlag uint16 -// setting types -const ( - TypeDefault settingType = iota // default setting and the only accepted setting -) - // setting flags offset const ( FlagInvalidOffset = iota @@ -168,14 +154,3 @@ func (f settingFlag) Enabled() bool { func (f settingFlag) TriggerTraceEnabled() bool { return f&FlagTriggerTrace != 0 } - -func (st settingType) toSampleSource() SampleSource { - var source SampleSource - switch st { - case TypeDefault: - source = SampleSourceDefault - default: - source = SampleSourceNone - } - return source -} diff --git a/internal/oboe/settings_lambda.go b/internal/oboe/settings_lambda.go new file mode 100644 index 00000000..a2f972d2 --- /dev/null +++ b/internal/oboe/settings_lambda.go @@ -0,0 +1,107 @@ +// © 2023 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 oboe + +import ( + "encoding/json" + "errors" + "io" + "os" + + "github.com/solarwinds/apm-go/internal/utils" +) + +type settingLambdaFromFile struct { + Arguments *settingArguments `json:"arguments"` + Flags string `json:"flags"` + Timestamp int64 `json:"timestamp"` + Ttl int64 `json:"ttl"` + Value int64 `json:"value"` +} + +type settingArguments struct { + BucketCapacity float64 `json:"BucketCapacity"` + BucketRate float64 `json:"BucketRate"` + MetricsFlushInterval int `json:"MetricsFlushInterval"` + TriggerRelaxedBucketCapacity float64 `json:"TriggerRelaxedBucketCapacity"` + TriggerRelaxedBucketRate float64 `json:"TriggerRelaxedBucketRate"` + TriggerStrictBucketCapacity float64 `json:"TriggerStrictBucketCapacity"` + TriggerStrictBucketRate float64 `json:"TriggerStrictBucketRate"` +} + +// N.B. this struct adheres to the types required by the oboe.Oboe interface. In the future, +// we should make the interface smarter about the incoming types so we're not converting from +// known types to []byte and back. A task has been created to track this work, though it also +// might make sense to do it when/if the GetSettings call is refactored. +type settingLambdaNormalized struct { + flags []byte + value int64 + ttl int64 + args map[string][]byte +} + +// newSettingLambdaNormalized accepts settings in json-unmarshalled format +// for mapping to a format readable by oboe UpdateSetting. +func newSettingLambdaNormalized(fromFile *settingLambdaFromFile) *settingLambdaNormalized { + flags := []byte(fromFile.Flags) + + var unusedToken = "TOKEN" + args := utils.ArgsToMap( + fromFile.Arguments.BucketCapacity, + fromFile.Arguments.BucketRate, + fromFile.Arguments.TriggerRelaxedBucketCapacity, + fromFile.Arguments.TriggerRelaxedBucketRate, + fromFile.Arguments.TriggerStrictBucketCapacity, + fromFile.Arguments.TriggerStrictBucketRate, + fromFile.Arguments.MetricsFlushInterval, + -1, + []byte(unusedToken), + ) + + settingNorm := &settingLambdaNormalized{ + flags, + fromFile.Value, + fromFile.Ttl, + args, + } + + return settingNorm +} + +// newSettingLambdaFromFile unmarshals sampling settings from a JSON file at a +// specific path in a specific format then returns values normalized for +// oboe UpdateSetting, else returns error. +func newSettingLambdaFromFile() (*settingLambdaNormalized, error) { + settingFile, err := os.Open(settingsFileName) + if err != nil { + return nil, err + } + settingBytes, err := io.ReadAll(settingFile) + if err != nil { + return nil, err + } + // Settings file should be an array with a single settings object + var settingLambdas []*settingLambdaFromFile + if err = json.Unmarshal(settingBytes, &settingLambdas); err != nil { + return nil, err + } + if len(settingLambdas) != 1 { + return nil, errors.New("settings file is incorrectly formatted") + } + + settingLambda := settingLambdas[0] + + return newSettingLambdaNormalized(settingLambda), nil +} diff --git a/internal/oboe/settings_lambda_test.go b/internal/oboe/settings_lambda_test.go new file mode 100644 index 00000000..a495c371 --- /dev/null +++ b/internal/oboe/settings_lambda_test.go @@ -0,0 +1,190 @@ +// © 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 oboe + +import ( + "github.com/solarwinds/apm-go/internal/constants" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestNewSettingLambdaNormalized(t *testing.T) { + settingArgs := settingArguments{ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + } + fromFile := settingLambdaFromFile{ + &settingArgs, + "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + 1715900164, + 120, + 1000000, + } + result := newSettingLambdaNormalized(&fromFile) + + assert.Equal( + t, + []byte{0x53, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x2c, 0x53, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x54, 0x48, 0x52, 0x4f, 0x55, 0x47, 0x48, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x2c, 0x53, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x45, 0x4e, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x2c, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45}, + result.flags, + ) + assert.Equal(t, result.value, int64(1000000)) + assert.Equal(t, result.ttl, int64(120)) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvBucketRate], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceRelaxedBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceRelaxedBucketRate], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceStrictBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceStrictBucketRate], + ) + assert.Equal( + t, + []byte{0x1, 0x0, 0x0, 0x0}, + result.args[constants.KvMetricsFlushInterval], + ) + assert.Equal( + t, + []byte(nil), + result.args[constants.KvMaxTransactions], + ) + assert.Equal( + t, + []byte{0x54, 0x4f, 0x4b, 0x45, 0x4e}, + result.args[constants.KvSignatureKey], + ) +} + +func TestNewSettingLambdaFromFileErrorOpen(t *testing.T) { + require.NoFileExists(t, settingsFileName) + res, err := newSettingLambdaFromFile() + assert.Nil(t, res) + assert.Error(t, err) +} + +func TestNewSettingLambdaFromFileErrorUnmarshal(t *testing.T) { + require.NoFileExists(t, settingsFileName) + + content := []byte("hello\ngo\n") + require.NoError(t, os.WriteFile(settingsFileName, content, 0644)) + res, err := newSettingLambdaFromFile() + assert.Nil(t, res) + assert.Error(t, err) + + os.Remove(settingsFileName) +} + +func TestNewSettingLambdaFromFileErrorLen(t *testing.T) { + require.NoFileExists(t, settingsFileName) + + content := []byte("[]") + require.NoError(t, os.WriteFile(settingsFileName, content, 0644)) + res, err := newSettingLambdaFromFile() + assert.Nil(t, res) + assert.Error(t, err) + + os.Remove(settingsFileName) +} + +func TestNewSettingLambdaFromFile(t *testing.T) { + require.NoFileExists(t, settingsFileName) + + content := []byte("[{\"arguments\":{\"BucketCapacity\":1,\"BucketRate\":1,\"MetricsFlushInterval\":1,\"TriggerRelaxedBucketCapacity\":1,\"TriggerRelaxedBucketRate\":1,\"TriggerStrictBucketCapacity\":1,\"TriggerStrictBucketRate\":1},\"flags\":\"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE\",\"layer\":\"\",\"timestamp\":1715900164,\"ttl\":120,\"type\":0,\"value\":1000000}]") + require.NoError(t, os.WriteFile(settingsFileName, content, 0644)) + result, err := newSettingLambdaFromFile() + assert.Nil(t, err) + assert.Equal( + t, + []byte{0x53, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x2c, 0x53, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x54, 0x48, 0x52, 0x4f, 0x55, 0x47, 0x48, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x2c, 0x53, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x5f, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x45, 0x4e, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x2c, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45}, + result.flags, + ) + assert.Equal(t, result.value, int64(1000000)) + assert.Equal(t, result.ttl, int64(120)) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvBucketRate], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceRelaxedBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceRelaxedBucketRate], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceStrictBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result.args[constants.KvTriggerTraceStrictBucketRate], + ) + assert.Equal( + t, + []byte{0x1, 0x0, 0x0, 0x0}, + result.args[constants.KvMetricsFlushInterval], + ) + assert.Equal( + t, + []byte(nil), + result.args[constants.KvMaxTransactions], + ) + assert.Equal( + t, + []byte{0x54, 0x4f, 0x4b, 0x45, 0x4e}, + result.args[constants.KvSignatureKey], + ) + + os.Remove(settingsFileName) +} diff --git a/internal/oboetestutils/oboe.go b/internal/oboetestutils/oboe.go index 3bd4a5cd..d317b7b4 100644 --- a/internal/oboetestutils/oboe.go +++ b/internal/oboetestutils/oboe.go @@ -15,120 +15,46 @@ package oboetestutils import ( - "encoding/binary" - "github.com/solarwinds/apm-go/internal/constants" - "math" + "github.com/solarwinds/apm-go/internal/utils" + "time" ) const TestToken = "TOKEN" -const TypeDefault = 0 - -func argsToMap(capacity, ratePerSec, tRCap, tRRate, tSCap, tSRate float64, - metricsFlushInterval, maxTransactions int, token []byte) map[string][]byte { - args := make(map[string][]byte) - - if capacity > -1 { - bits := math.Float64bits(capacity) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - args[constants.KvBucketCapacity] = bytes - } - if ratePerSec > -1 { - bits := math.Float64bits(ratePerSec) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - args[constants.KvBucketRate] = bytes - } - if tRCap > -1 { - bits := math.Float64bits(tRCap) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - args[constants.KvTriggerTraceRelaxedBucketCapacity] = bytes - } - if tRRate > -1 { - bits := math.Float64bits(tRRate) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - args[constants.KvTriggerTraceRelaxedBucketRate] = bytes - } - if tSCap > -1 { - bits := math.Float64bits(tSCap) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - args[constants.KvTriggerTraceStrictBucketCapacity] = bytes - } - if tSRate > -1 { - bits := math.Float64bits(tSRate) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - args[constants.KvTriggerTraceStrictBucketRate] = bytes - } - if metricsFlushInterval > -1 { - bytes := make([]byte, 4) - binary.LittleEndian.PutUint32(bytes, uint32(metricsFlushInterval)) - args[constants.KvMetricsFlushInterval] = bytes - } - if maxTransactions > -1 { - bytes := make([]byte, 4) - binary.LittleEndian.PutUint32(bytes, uint32(maxTransactions)) - args[constants.KvMaxTransactions] = bytes - } - - args[constants.KvSignatureKey] = token - - return args -} type SettingUpdater interface { - UpdateSetting(sType int32, layer string, flags []byte, value int64, ttl int64, args map[string][]byte) + UpdateSetting(flags []byte, value int64, ttl time.Duration, args map[string][]byte) } func AddDefaultSetting(o SettingUpdater) { // add default setting with 100% sampling - o.UpdateSetting(int32(TypeDefault), "", - []byte("SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE"), - 1000000, 120, argsToMap(1000000, 1000000, 1000000, 1000000, 1000000, 1000000, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE"), 1000000, 120, utils.ArgsToMap(1000000, 1000000, 1000000, 1000000, 1000000, 1000000, -1, -1, []byte(TestToken))) } func AddSampleThrough(o SettingUpdater) { // add default setting with 100% sampling - o.UpdateSetting(int32(TypeDefault), "", - []byte("SAMPLE_START,SAMPLE_THROUGH,TRIGGER_TRACE"), - 1000000, 120, argsToMap(1000000, 1000000, 1000000, 1000000, 1000000, 1000000, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("SAMPLE_START,SAMPLE_THROUGH,TRIGGER_TRACE"), 1000000, 120, utils.ArgsToMap(1000000, 1000000, 1000000, 1000000, 1000000, 1000000, -1, -1, []byte(TestToken))) } func AddNoTriggerTrace(o SettingUpdater) { - o.UpdateSetting(int32(TypeDefault), "", - []byte("SAMPLE_START,SAMPLE_THROUGH_ALWAYS"), - 1000000, 120, argsToMap(1000000, 1000000, 0, 0, 0, 0, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("SAMPLE_START,SAMPLE_THROUGH_ALWAYS"), 1000000, 120, utils.ArgsToMap(1000000, 1000000, 0, 0, 0, 0, -1, -1, []byte(TestToken))) } func AddTriggerTraceOnly(o SettingUpdater) { - o.UpdateSetting(int32(TypeDefault), "", - []byte("TRIGGER_TRACE"), - 0, 120, argsToMap(0, 0, 1000000, 1000000, 1000000, 1000000, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("TRIGGER_TRACE"), 0, 120, utils.ArgsToMap(0, 0, 1000000, 1000000, 1000000, 1000000, -1, -1, []byte(TestToken))) } func AddRelaxedTriggerTraceOnly(o SettingUpdater) { - o.UpdateSetting(int32(TypeDefault), "", - []byte("TRIGGER_TRACE"), - 0, 120, argsToMap(0, 0, 1000000, 1000000, 0, 0, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("TRIGGER_TRACE"), 0, 120, utils.ArgsToMap(0, 0, 1000000, 1000000, 0, 0, -1, -1, []byte(TestToken))) } func AddStrictTriggerTraceOnly(o SettingUpdater) { - o.UpdateSetting(int32(TypeDefault), "", - []byte("TRIGGER_TRACE"), - 0, 120, argsToMap(0, 0, 0, 0, 1000000, 1000000, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("TRIGGER_TRACE"), 0, 120, utils.ArgsToMap(0, 0, 0, 0, 1000000, 1000000, -1, -1, []byte(TestToken))) } func AddLimitedTriggerTrace(o SettingUpdater) { - o.UpdateSetting(int32(TypeDefault), "", - []byte("SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE"), - 1000000, 120, argsToMap(1000000, 1000000, 1, 1, 1, 1, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte("SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE"), 1000000, 120, utils.ArgsToMap(1000000, 1000000, 1, 1, 1, 1, -1, -1, []byte(TestToken))) } func AddDisabled(o SettingUpdater) { - o.UpdateSetting(int32(TypeDefault), "", - []byte(""), - 0, 120, argsToMap(0, 0, 1, 1, 1, 1, -1, -1, []byte(TestToken))) + o.UpdateSetting([]byte(""), 0, 120, utils.ArgsToMap(0, 0, 1, 1, 1, 1, -1, -1, []byte(TestToken))) } diff --git a/internal/processor/processor.go b/internal/processor/processor.go index f581d389..de7a0118 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -22,18 +22,16 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -func NewInboundMetricsSpanProcessor(registry metrics.MetricRegistry, isAppoptics bool) sdktrace.SpanProcessor { +func NewInboundMetricsSpanProcessor(registry metrics.MetricRegistry) sdktrace.SpanProcessor { return &inboundMetricsSpanProcessor{ - registry: registry, - isAppoptics: isAppoptics, + registry: registry, } } var _ sdktrace.SpanProcessor = &inboundMetricsSpanProcessor{} type inboundMetricsSpanProcessor struct { - registry metrics.MetricRegistry - isAppoptics bool + registry metrics.MetricRegistry } func (s *inboundMetricsSpanProcessor) OnStart(_ context.Context, span sdktrace.ReadWriteSpan) { @@ -62,7 +60,7 @@ func maybeClearEntrySpan(span sdktrace.ReadOnlySpan) { func (s *inboundMetricsSpanProcessor) OnEnd(span sdktrace.ReadOnlySpan) { if entryspans.IsEntrySpan(span) { - s.registry.RecordSpan(span, s.isAppoptics) + s.registry.RecordSpan(span) maybeClearEntrySpan(span) } } diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index c6edba46..c765af3f 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -26,18 +26,16 @@ import ( ) type recordMock struct { - span sdktrace.ReadOnlySpan - isAppoptics bool - called bool + span sdktrace.ReadOnlySpan + called bool } -func (r *recordMock) RecordSpan(span sdktrace.ReadOnlySpan, isAppoptics bool) { +func (r *recordMock) RecordSpan(span sdktrace.ReadOnlySpan) { r.span = span - r.isAppoptics = isAppoptics r.called = true } -func (r *recordMock) BuildBuiltinMetricsMessage(int32, *metrics.EventQueueStats, map[string]*metrics.RateCounts, bool) []byte { +func (r *recordMock) BuildBuiltinMetricsMessage(int32, *metrics.EventQueueStats, *metrics.RateCountSummary, bool) []byte { panic("should not be called in this test") } @@ -65,7 +63,7 @@ var _ metrics.LegacyRegistry = &recordMock{} func TestInboundMetricsSpanProcessorOnEnd(t *testing.T) { mock := &recordMock{} - sp := NewInboundMetricsSpanProcessor(mock, false) + sp := NewInboundMetricsSpanProcessor(mock) tp := sdktrace.NewTracerProvider( sdktrace.WithSpanProcessor(sp), sdktrace.WithSampler(sdktrace.AlwaysSample()), @@ -86,7 +84,6 @@ func TestInboundMetricsSpanProcessorOnEnd(t *testing.T) { require.True(t, ok) require.Equal(t, s.SpanContext().SpanID(), es) assert.True(t, mock.called) - assert.False(t, mock.isAppoptics) } type recordOnlySampler struct{} @@ -104,7 +101,7 @@ func (ro recordOnlySampler) Description() string { func TestInboundMetricsSpanProcessorOnEndRecordOnly(t *testing.T) { mock := &recordMock{} - sp := NewInboundMetricsSpanProcessor(mock, false) + sp := NewInboundMetricsSpanProcessor(mock) tp := sdktrace.NewTracerProvider( sdktrace.WithSpanProcessor(sp), sdktrace.WithSampler(recordOnlySampler{}), @@ -125,12 +122,11 @@ func TestInboundMetricsSpanProcessorOnEndRecordOnly(t *testing.T) { require.False(t, ok) require.False(t, es.IsValid()) assert.True(t, mock.called) - assert.False(t, mock.isAppoptics) } func TestInboundMetricsSpanProcessorOnEndWithLocalParent(t *testing.T) { mock := &recordMock{} - sp := NewInboundMetricsSpanProcessor(mock, false) + sp := NewInboundMetricsSpanProcessor(mock) tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sp)) tracer := tp.Tracer("foo") ctx, s1 := tracer.Start(context.Background(), "span name") @@ -153,7 +149,7 @@ func TestInboundMetricsSpanProcessorOnEndWithLocalParent(t *testing.T) { func TestInboundMetricsSpanProcessorOnEndWithRemoteParent(t *testing.T) { mock := &recordMock{} - sp := NewInboundMetricsSpanProcessor(mock, false) + sp := NewInboundMetricsSpanProcessor(mock) tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sp)) tracer := tp.Tracer("foo") ctx := context.Background() diff --git a/internal/reporter/reporter_grpc.go b/internal/reporter/reporter_grpc.go index b0ced062..56dfea76 100644 --- a/internal/reporter/reporter_grpc.go +++ b/internal/reporter/reporter_grpc.go @@ -890,7 +890,7 @@ func (r *grpcReporter) getSettings() (*collector.SettingsResult, error) { // settings new settings func (r *grpcReporter) updateSettings(settings *collector.SettingsResult) { for _, s := range settings.GetSettings() { - r.oboe.UpdateSetting(int32(s.Type), string(s.Layer), s.Flags, s.Value, s.Ttl, s.Arguments) + r.oboe.UpdateSetting(s.Flags, s.Value, time.Duration(s.Ttl)*time.Second, s.Arguments) // update MetricsFlushInterval mi := ParseInt32(s.Arguments, constants.KvMetricsFlushInterval, r.collectMetricInterval) @@ -1280,7 +1280,7 @@ func (d *DefaultDialer) Dial(p DialParams) (*grpc.ClientConn, error) { opts = append(opts, grpc.WithContextDialer(newGRPCProxyDialer(p))) } - return grpc.Dial(p.Address, opts...) + return grpc.NewClient(p.Address, opts...) } func newGRPCProxyDialer(p DialParams) func(context.Context, string) (net.Conn, error) { diff --git a/internal/reporter/reporter_grpc_test.go b/internal/reporter/reporter_grpc_test.go index e9809363..25d583d2 100644 --- a/internal/reporter/reporter_grpc_test.go +++ b/internal/reporter/reporter_grpc_test.go @@ -135,7 +135,6 @@ func (s *TestGRPCServer) GetSettings(ctx context.Context, req *pb.SettingsReques Settings: []*pb.OboeSetting{{ Type: pb.OboeSettingType_DEFAULT_SAMPLE_RATE, // Flags: XXX, - // Layer: "", // default, specifically not setting layer/service // Timestamp: XXX, Value: 1000000, Arguments: map[string][]byte{ diff --git a/internal/reporter/reporter_test.go b/internal/reporter/reporter_test.go index b187dc10..f3c1548d 100644 --- a/internal/reporter/reporter_test.go +++ b/internal/reporter/reporter_test.go @@ -89,7 +89,7 @@ func TestGRPCReporter(t *testing.T) { setEnv("SW_APM_COLLECTOR", addr) setEnv("SW_APM_TRUSTEDPATH", testCertFile) config.Load() - registry := metrics.NewLegacyRegistry() + registry := metrics.NewLegacyRegistry(false) o := oboe.NewOboe() r := newGRPCReporter("myservice", registry, o).(*grpcReporter) @@ -176,7 +176,7 @@ func TestShutdownGRPCReporter(t *testing.T) { setEnv("SW_APM_COLLECTOR", addr) setEnv("SW_APM_TRUSTEDPATH", testCertFile) config.Load() - registry := metrics.NewLegacyRegistry() + registry := metrics.NewLegacyRegistry(false) o := oboe.NewOboe() r := newGRPCReporter("myservice", registry, o).(*grpcReporter) r.ShutdownNow() @@ -236,7 +236,7 @@ func TestInvalidKey(t *testing.T) { config.Load() log.SetLevel(log.INFO) - registry := metrics.NewLegacyRegistry() + registry := metrics.NewLegacyRegistry(false) o := oboe.NewOboe() r := newGRPCReporter("myservice", registry, o).(*grpcReporter) @@ -447,7 +447,7 @@ func TestInitReporter(t *testing.T) { // Test disable agent setEnv("SW_APM_ENABLED", "false") config.Load() - registry := metrics.NewLegacyRegistry() + registry := metrics.NewLegacyRegistry(false) o := oboe.NewOboe() r := initReporter(resource.Empty(), registry, o) require.IsType(t, &nullReporter{}, r) @@ -493,7 +493,7 @@ func testProxy(t *testing.T, proxyUrl string) { server := StartTestGRPCServer(t, addr) time.Sleep(100 * time.Millisecond) - registry := metrics.NewLegacyRegistry() + registry := metrics.NewLegacyRegistry(false) o := oboe.NewOboe() r := newGRPCReporter("myservice", registry, o).(*grpcReporter) diff --git a/internal/utils/otel.go b/internal/txn/txn.go similarity index 65% rename from internal/utils/otel.go rename to internal/txn/txn.go index 9e10eaf0..45b8a6ed 100644 --- a/internal/utils/otel.go +++ b/internal/txn/txn.go @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package utils +package txn import ( + "github.com/solarwinds/apm-go/internal/config" "github.com/solarwinds/apm-go/internal/entryspans" "github.com/solarwinds/apm-go/internal/swotel/semconv" "go.opentelemetry.io/otel/attribute" @@ -35,32 +36,36 @@ func GetTransactionName(span sdktrace.ReadOnlySpan) string { // deriveTransactionName returns transaction name from given span name and attributes, falling back to "unknown" func deriveTransactionName(name string, attrs []attribute.KeyValue) string { - var httpRoute, httpUrl, txnName = "", "", "" - for _, attr := range attrs { - if attr.Key == semconv.HTTPRouteKey { - httpRoute = attr.Value.AsString() - } else if attr.Key == semconv.HTTPURLKey { - httpUrl = attr.Value.AsString() + txnName := config.GetTransactionName() + if txnName == "" { + var httpRoute, httpUrl = "", "" + for _, attr := range attrs { + if attr.Key == semconv.HTTPRouteKey { + httpRoute = attr.Value.AsString() + } else if attr.Key == semconv.HTTPURLKey { + httpUrl = attr.Value.AsString() + } } - } - if httpRoute != "" { - txnName = httpRoute - } else if name != "" { - txnName = name - } - if httpUrl != "" && strings.TrimSpace(txnName) == "" { - parsed, err := url.Parse(httpUrl) - if err != nil { - // We can't import internal logger in the util package, so we default to "log". However, this should be - // infrequent. - log.Println("could not parse URL from span", httpUrl) - } else { - // Clear user/password - parsed.User = nil - txnName = parsed.String() + if httpRoute != "" { + txnName = httpRoute + } else if name != "" { + txnName = name + } + if httpUrl != "" && strings.TrimSpace(txnName) == "" { + parsed, err := url.Parse(httpUrl) + if err != nil { + // We can't import internal logger in the util package, so we default to "log". However, this should be + // infrequent. + log.Println("could not parse URL from span", httpUrl) + } else { + // Clear user/password + parsed.User = nil + txnName = parsed.String() + } } } + txnName = strings.TrimSpace(txnName) if txnName == "" { txnName = "unknown" diff --git a/internal/utils/otel_test.go b/internal/txn/txn_test.go similarity index 66% rename from internal/utils/otel_test.go rename to internal/txn/txn_test.go index aaf79e73..b1ddbd1b 100644 --- a/internal/utils/otel_test.go +++ b/internal/txn/txn_test.go @@ -12,15 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package utils +package txn import ( "context" + "github.com/solarwinds/apm-go/internal/config" "github.com/solarwinds/apm-go/internal/entryspans" "github.com/solarwinds/apm-go/internal/testutils" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/trace" + "os" "strings" "testing" ) @@ -87,3 +89,42 @@ func TestDeriveTransactionName(t *testing.T) { expected := strings.Repeat("a", 255) require.Equal(t, expected, deriveTransactionName(name, attrs)) } + +func TestDeriveTxnFromEnv(t *testing.T) { + envTxn := "env-provided" + name := "span name" + var attrs []attribute.KeyValue + // `SW_APM_TRANSACTION_NAME` only takes effect in Lambda + require.NoError(t, os.Setenv("SW_APM_TRANSACTION_NAME", envTxn)) + config.Load() + require.Equal(t, "", config.GetTransactionName()) + require.Equal(t, "span name", deriveTransactionName(name, attrs)) + + require.NoError(t, os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "foo")) + require.NoError(t, os.Setenv("LAMBDA_TASK_ROOT", "bar")) + defer func() { + _ = os.Unsetenv("SW_APM_TRANSACTION_NAME") + _ = os.Unsetenv("AWS_LAMBDA_FUNCTION_NAME") + _ = os.Unsetenv("LAMBDA_TASK_ROOT") + }() + config.Load() + require.Equal(t, envTxn, config.GetTransactionName()) + require.Equal(t, envTxn, deriveTransactionName(name, attrs)) +} +func TestDeriveTxnFromEnvTruncated(t *testing.T) { + envTxn := strings.Repeat("a", 1024) + expected := strings.Repeat("a", 255) + name := "span name" + var attrs []attribute.KeyValue + require.NoError(t, os.Setenv("SW_APM_TRANSACTION_NAME", envTxn)) + require.NoError(t, os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "foo")) + require.NoError(t, os.Setenv("LAMBDA_TASK_ROOT", "bar")) + defer func() { + _ = os.Unsetenv("SW_APM_TRANSACTION_NAME") + _ = os.Unsetenv("AWS_LAMBDA_FUNCTION_NAME") + _ = os.Unsetenv("LAMBDA_TASK_ROOT") + }() + config.Load() + require.Equal(t, envTxn, config.GetTransactionName()) + require.Equal(t, expected, deriveTransactionName(name, attrs)) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 36e9c1f6..5f73755c 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -17,12 +17,15 @@ package utils import ( "bufio" "bytes" + "encoding/binary" "encoding/json" + "math" "os" "runtime" "strings" "sync" + "github.com/solarwinds/apm-go/internal/constants" "gopkg.in/mgo.v2/bson" ) @@ -133,6 +136,64 @@ func IsHigherOrEqualGoVersion(version string) bool { return true } +// ArgsToMap uses settings as float/int/bytes to create a map of string keys +// to bytes, for usability by oboe UpdateSetting. +func ArgsToMap(capacity, ratePerSec, tRCap, tRRate, tSCap, tSRate float64, + metricsFlushInterval, maxTransactions int, token []byte) map[string][]byte { + args := make(map[string][]byte) + + if capacity > -1 { + bits := math.Float64bits(capacity) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + args[constants.KvBucketCapacity] = bytes + } + if ratePerSec > -1 { + bits := math.Float64bits(ratePerSec) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + args[constants.KvBucketRate] = bytes + } + if tRCap > -1 { + bits := math.Float64bits(tRCap) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + args[constants.KvTriggerTraceRelaxedBucketCapacity] = bytes + } + if tRRate > -1 { + bits := math.Float64bits(tRRate) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + args[constants.KvTriggerTraceRelaxedBucketRate] = bytes + } + if tSCap > -1 { + bits := math.Float64bits(tSCap) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + args[constants.KvTriggerTraceStrictBucketCapacity] = bytes + } + if tSRate > -1 { + bits := math.Float64bits(tSRate) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + args[constants.KvTriggerTraceStrictBucketRate] = bytes + } + if metricsFlushInterval > -1 { + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, uint32(metricsFlushInterval)) + args[constants.KvMetricsFlushInterval] = bytes + } + if maxTransactions > -1 { + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, uint32(maxTransactions)) + args[constants.KvMaxTransactions] = bytes + } + + args[constants.KvSignatureKey] = token + + return args +} + // SafeBuffer is goroutine-safe buffer. It is for internal test use only. type SafeBuffer struct { buf bytes.Buffer diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 00000000..0b48f129 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,124 @@ +// © 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 utils + +import ( + "testing" + + "github.com/solarwinds/apm-go/internal/constants" + "github.com/stretchr/testify/assert" +) + +const TestToken = "TOKEN" + +func TestArgsToMapAllSet(t *testing.T) { + result := ArgsToMap(1, 1, 1, 1, 1, 1, 1, 1, []byte(TestToken)) + + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result[constants.KvBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result[constants.KvBucketRate], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result[constants.KvTriggerTraceRelaxedBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result[constants.KvTriggerTraceRelaxedBucketRate], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result[constants.KvTriggerTraceStrictBucketCapacity], + ) + assert.Equal( + t, + []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf0, 0x3f}, + result[constants.KvTriggerTraceStrictBucketRate], + ) + assert.Equal( + t, + []byte{0x1, 0x0, 0x0, 0x0}, + result[constants.KvMetricsFlushInterval], + ) + assert.Equal( + t, + []byte{0x1, 0x0, 0x0, 0x0}, + result[constants.KvMaxTransactions], + ) + assert.Equal( + t, + []byte{0x54, 0x4f, 0x4b, 0x45, 0x4e}, + result[constants.KvSignatureKey], + ) +} + +func TestArgsToMapAllUnset(t *testing.T) { + result := ArgsToMap(-1, -1, -1, -1, -1, -1, -1, -1, []byte(TestToken)) + + assert.Equal( + t, + []byte(nil), + result[constants.KvBucketCapacity], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvBucketRate], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvTriggerTraceRelaxedBucketCapacity], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvTriggerTraceRelaxedBucketRate], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvTriggerTraceStrictBucketCapacity], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvTriggerTraceStrictBucketRate], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvMetricsFlushInterval], + ) + assert.Equal( + t, + []byte(nil), + result[constants.KvMaxTransactions], + ) + assert.Equal( + t, + []byte{0x54, 0x4f, 0x4b, 0x45, 0x4e}, + result[constants.KvSignatureKey], + ) +} diff --git a/swo/agent.go b/swo/agent.go index 5500e7a6..9e863132 100644 --- a/swo/agent.go +++ b/swo/agent.go @@ -16,6 +16,9 @@ package swo import ( "context" + "os" + + "github.com/pkg/errors" "github.com/solarwinds/apm-go/internal/config" "github.com/solarwinds/apm-go/internal/entryspans" "github.com/solarwinds/apm-go/internal/exporter" @@ -26,17 +29,19 @@ import ( "github.com/solarwinds/apm-go/internal/propagator" "github.com/solarwinds/apm-go/internal/reporter" "github.com/solarwinds/apm-go/internal/sampler" + "github.com/solarwinds/apm-go/internal/utils" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" "io" stdlog "log" "strings" - - "github.com/pkg/errors" ) var ( @@ -88,7 +93,8 @@ func Start(resourceAttrs ...attribute.KeyValue) (func(), error) { // return a no-op func so that we don't cause a nil-deref for the end-user }, err } - registry := metrics.NewLegacyRegistry() + isAppoptics := strings.Contains(strings.ToLower(config.GetCollector()), "appoptics.com") + registry := metrics.NewLegacyRegistry(isAppoptics) o := oboe.NewOboe() _reporter, err := reporter.Start(resrc, registry, o) if err != nil { @@ -101,8 +107,7 @@ func Start(resourceAttrs ...attribute.KeyValue) (func(), error) { return func() {}, err } config.Load() - isAppoptics := strings.Contains(strings.ToLower(config.GetCollector()), "appoptics.com") - proc := processor.NewInboundMetricsSpanProcessor(registry, isAppoptics) + proc := processor.NewInboundMetricsSpanProcessor(registry) prop := propagation.NewCompositeTextMapPropagator( &propagation.TraceContext{}, &propagation.Baggage{}, @@ -137,3 +142,97 @@ func SetTransactionName(ctx context.Context, name string) error { } return entryspans.SetTransactionName(sc.TraceID(), name) } + +type Flusher interface { + Flush(ctx context.Context) error +} + +type lambdaFlusher struct { + Reader *metric.PeriodicReader +} + +func (l lambdaFlusher) Flush(ctx context.Context) error { + return l.Reader.ForceFlush(ctx) +} + +var _ Flusher = &lambdaFlusher{} + +func StartLambda(lambdaLogStreamName string) (Flusher, error) { + // By default, the Go OTEL SDK sets this to `https://localhost:4317`, however + // we do not use https for the local collector in Lambda. We override if not + // already set. + if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") == "" { + if err := os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"); err != nil { + log.Warningf("could not override unset OTEL_EXPORTER_OTLP_ENDPOINT %s", err) + } + } + ctx := context.Background() + o := oboe.NewOboe() + settingsWatcher := oboe.NewFileBasedWatcher(o) + // settingsWatcher is started but never stopped in Lambda + settingsWatcher.Start() + var err error + var tpOpts []sdktrace.TracerProviderOption + var metExp metric.Exporter + if metExp, err = otlpmetricgrpc.New(ctx, + otlpmetricgrpc.WithTemporalitySelector(metrics.TemporalitySelector), + ); err != nil { + return nil, err + } + // The reader is flushed manually + reader := metric.NewPeriodicReader(metExp) + // The flusher is called after every invocation. We only need to flush + // metrics here because traces are sent synchronously. + flusher := &lambdaFlusher{ + Reader: reader, + } + mp := metric.NewMeterProvider( + metric.WithReader(reader), + ) + otel.SetMeterProvider(mp) + if err = o.RegisterOtelSampleRateMetrics(mp); err != nil { + return nil, err + } + if exprtr, err := otlptracegrpc.New(ctx); err != nil { + return nil, err + } else { + // Use WithSyncer to flush all spans each invocation + tpOpts = append(tpOpts, sdktrace.WithSyncer(exprtr)) + } + registry, err := metrics.NewOtelRegistry(mp) + if err != nil { + return nil, err + } + proc := processor.NewInboundMetricsSpanProcessor(registry) + prop := propagation.NewCompositeTextMapPropagator( + &propagation.TraceContext{}, + &propagation.Baggage{}, + &propagator.SolarwindsPropagator{}, + ) + smplr, err := sampler.NewSampler(o) + if err != nil { + return nil, err + } + otel.SetTextMapPropagator(prop) + // Default resource detection plus our required attributes + var resrc *resource.Resource + resrc, err = resource.Merge( + resource.Default(), + resource.NewSchemaless( + attribute.String("sw.data.module", "apm"), + attribute.String("sw.apm.version", utils.Version()), + attribute.String("faas.instance", lambdaLogStreamName), + ), + ) + if err != nil { + return nil, err + } + + tpOpts = append(tpOpts, + sdktrace.WithResource(resrc), + sdktrace.WithSampler(smplr), + sdktrace.WithSpanProcessor(proc), + ) + otel.SetTracerProvider(sdktrace.NewTracerProvider(tpOpts...)) + return flusher, nil +}