From d0b608af3178bc674936f4b1c6cce00591ebbf09 Mon Sep 17 00:00:00 2001 From: Andrii Kushch Date: Tue, 8 Mar 2022 20:18:32 +0100 Subject: [PATCH] tracing: New OpenTelemetry module (#4361) * opentelemetry: create a new module * fix imports * fix test * Update modules/caddyhttp/opentelemetry/README.md Co-authored-by: Dave Henderson * Update modules/caddyhttp/opentelemetry/README.md Co-authored-by: Dave Henderson * Update modules/caddyhttp/opentelemetry/README.md Co-authored-by: Dave Henderson * Update modules/caddyhttp/opentelemetry/tracer.go Co-authored-by: Dave Henderson * rename error ErrUnsupportedTracesProtocol * replace spaces with tabs in the test data * Update modules/caddyhttp/opentelemetry/README.md Co-authored-by: Francis Lavoie * Update modules/caddyhttp/opentelemetry/README.md Co-authored-by: Francis Lavoie * replace spaces with tabs in the README.md * use default values for a propagation and exporter protocol * set http attributes with helper * simplify code * Cleanup modules/caddyhttp/opentelemetry/README.md Co-authored-by: Dave Henderson * Update link in README.md Co-authored-by: Dave Henderson * Update documentation in README.md Co-authored-by: Dave Henderson * Update link to naming spec in README.md Co-authored-by: Dave Henderson * Rename module from opentelemetry to tracing Co-authored-by: Dave Henderson * Rename span_name to span Co-authored-by: Dave Henderson * Rename span_name to span Co-authored-by: Dave Henderson * Simplify otel resource creation Co-authored-by: Dave Henderson * handle extra attributes Co-authored-by: Dave Henderson * update go.opentelemetry.io/otel/semconv to 1.7.0 Co-authored-by: Dave Henderson * update go.opentelemetry.io/otel version * remove environment variable handling * always use tracecontext,baggage as propagators * extract tracer name into variable * rename OpenTelemetry to Tracing * simplify resource creation * update go.mod * rename package from opentelemetry to tracing * cleanup tests * update Caddyfile example in README.md * update README.md * fix test * fix module name in README.md * fix module name in README.md * change names in README.md and tests * order imports * remove redundant tests * Update documentation README.md Co-authored-by: Dave Henderson * Fix grammar Co-authored-by: Dave Henderson * Update comments Co-authored-by: Dave Henderson * Update comments Co-authored-by: Dave Henderson * update go.sum * update go.sum * Add otelhttp instrumentation, update OpenTelemetry libraries. * Use otelhttp instrumentation for instrumenting HTTP requests. This change uses context.WithValue to inject the next handler into the request context via a "nextCall" carrier struct, and pass it on to a standard Go HTTP handler returned by otelhttp.NewHandler. The underlying handler will extract the next handler from the context, call it and pass the returned error to the carrier struct. * use zap.Error() for the error log * remove README.md * update dependencies * clean up the code * change comment * move serveHTTP method from separate file * add syntax to the UnmarshalCaddyfile comment * go import the file * admin: Write proper status on invalid requests (#4569) (fix #4561) * update dependencies Co-authored-by: Dave Henderson Co-authored-by: Francis Lavoie Co-authored-by: Vibhav Pant Co-authored-by: Alok Naushad Co-authored-by: Cedric Ziel --- caddyconfig/httpcaddyfile/directives.go | 2 + .../integration/caddyfile_adapt/tracing.txt | 36 ++++ go.mod | 4 + go.sum | 47 ++++- modules/caddyhttp/standard/imports.go | 1 + modules/caddyhttp/tracing/module.go | 121 ++++++++++++ modules/caddyhttp/tracing/module_test.go | 182 ++++++++++++++++++ modules/caddyhttp/tracing/tracer.go | 108 +++++++++++ modules/caddyhttp/tracing/tracer_test.go | 27 +++ modules/caddyhttp/tracing/tracerprovider.go | 63 ++++++ .../caddyhttp/tracing/tracerprovider_test.go | 43 +++++ 11 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/tracing.txt create mode 100644 modules/caddyhttp/tracing/module.go create mode 100644 modules/caddyhttp/tracing/module_test.go create mode 100644 modules/caddyhttp/tracing/tracer.go create mode 100644 modules/caddyhttp/tracing/tracer_test.go create mode 100644 modules/caddyhttp/tracing/tracerprovider.go create mode 100644 modules/caddyhttp/tracing/tracerprovider_test.go diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index feb8e91a..03e6753e 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -37,6 +37,8 @@ import ( // The header directive goes second so that headers // can be manipulated before doing redirects. var directiveOrder = []string{ + "tracing", + "map", "root", diff --git a/caddytest/integration/caddyfile_adapt/tracing.txt b/caddytest/integration/caddyfile_adapt/tracing.txt new file mode 100644 index 00000000..32286600 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tracing.txt @@ -0,0 +1,36 @@ +:80 { + tracing /myhandler { + span my-span + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "match": [ + { + "path": [ + "/myhandler" + ] + } + ], + "handle": [ + { + "handler": "tracing", + "span": "my-span" + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 511d2a84..61e2360f 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,10 @@ require ( github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74 github.com/yuin/goldmark v1.4.4 github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 + go.opentelemetry.io/otel v1.4.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 + go.opentelemetry.io/otel/sdk v1.4.0 go.uber.org/zap v1.20.0 golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 golang.org/x/net v0.0.0-20210913180222-943fd674d43e diff --git a/go.sum b/go.sum index 2107d539..ff454d44 100644 --- a/go.sum +++ b/go.sum @@ -194,7 +194,10 @@ github.com/caddyserver/certmagic v0.15.4/go.mod h1:qhkAOthf72ufAcp3Y5jF2RaGE96oi github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -218,7 +221,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= @@ -289,11 +296,14 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -330,6 +340,10 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -412,8 +426,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= @@ -489,6 +504,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= @@ -1001,17 +1017,39 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 h1:SLme4Porm+UwX0DdHMxlwRt7FzPSE0sys81bet2o0pU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0/go.mod h1:tLYsuf2v8fZreBVwp9gVMhefZlLFZaUiNVSq8QxXRII= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.4.0 h1:7ESuKPq6zpjRaY5nvVDGiuwK7VAJ8MwkKnmNJ9whNZ4= +go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= +go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 h1:j7AwzDdAQBJjcqayAaYbvpYeZzII7cEe5qJTu+De6UY= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 h1:lRpP10E8oTGVmY1nVXcwelCT1Z8ca41/l5ce7AqLAss= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0/go.mod h1:3oS+j2WUoJVyj6/BzQN/52G17lNJDulngsOxDm1w2PY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 h1:buSx4AMC/0Z232slPhicN/fU5KIlj0bMngct5pcZhkI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0/go.mod h1:ew1NcwkHo0QFT3uTm3m2IVZMkZdVIpbOYNPasgWwpdk= +go.opentelemetry.io/otel/internal/metric v0.27.0 h1:9dAVGAfFiiEq5NVB9FUJ5et+btbDQAUIJehJ+ikyryk= +go.opentelemetry.io/otel/internal/metric v0.27.0/go.mod h1:n1CVxRqKqYZtqyTh9U/onvKapPGv7y/rpyOTI+LFNzw= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/metric v0.27.0 h1:HhJPsGhJoKRSegPQILFbODU56NS/L1UE4fS1sC5kIwQ= +go.opentelemetry.io/otel/metric v0.27.0/go.mod h1:raXDJ7uP2/Jc0nVZWQjJtzoyssOYWu/+pjZqRzfvZ7g= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.4.0 h1:LJE4SW3jd4lQTESnlpQZcBhQ3oci0U2MLR5uhicfTHQ= +go.opentelemetry.io/otel/sdk v1.4.0/go.mod h1:71GJPNJh4Qju6zJuYl1CrYtXbrgfau/M9UAggqiy1UE= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.4.0 h1:4OOUrPZdVFQkbzl/JSdvGCWIdw5ONXXxzHlaLlWppmo= +go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.12.0 h1:CMJ/3Wp7iOWES+CYLfnBv+DVmPbB+kmy9PJ92XvlR6c= +go.opentelemetry.io/proto/otlp v0.12.0/go.mod h1:TsIjwGWIx5VFYv9KGVlOpxoBl5Dy+63SUguV7GGvlSQ= go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= @@ -1026,8 +1064,9 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -1279,6 +1318,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1557,6 +1597,9 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/modules/caddyhttp/standard/imports.go b/modules/caddyhttp/standard/imports.go index 0e2203c1..8ce23957 100644 --- a/modules/caddyhttp/standard/imports.go +++ b/modules/caddyhttp/standard/imports.go @@ -17,4 +17,5 @@ import ( _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" + _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing" ) diff --git a/modules/caddyhttp/tracing/module.go b/modules/caddyhttp/tracing/module.go new file mode 100644 index 00000000..7cce6692 --- /dev/null +++ b/modules/caddyhttp/tracing/module.go @@ -0,0 +1,121 @@ +package tracing + +import ( + "fmt" + "net/http" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(Tracing{}) + httpcaddyfile.RegisterHandlerDirective("tracing", parseCaddyfile) +} + +// Tracing implements an HTTP handler that adds support for distributed tracing, +// using OpenTelemetry. This module is responsible for the injection and +// propagation of the trace context. Configure this module via environment +// variables (see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md). +// Some values can be overwritten in the configuration file. +type Tracing struct { + // SpanName is a span name. It should follow the naming guidelines here: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span + SpanName string `json:"span"` + + // otel implements opentelemetry related logic. + otel openTelemetryWrapper + + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (Tracing) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.handlers.tracing", + New: func() caddy.Module { return new(Tracing) }, + } +} + +// Provision implements caddy.Provisioner. +func (ot *Tracing) Provision(ctx caddy.Context) error { + ot.logger = ctx.Logger(ot) + + var err error + ot.otel, err = newOpenTelemetryWrapper(ctx, ot.SpanName) + + return err +} + +// Cleanup implements caddy.CleanerUpper and closes any idle connections. It +// calls Shutdown method for a trace provider https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shutdown. +func (ot *Tracing) Cleanup() error { + if err := ot.otel.cleanup(ot.logger); err != nil { + return fmt.Errorf("tracerProvider shutdown: %w", err) + } + return nil +} + +// ServeHTTP implements caddyhttp.MiddlewareHandler. +func (ot *Tracing) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + return ot.otel.ServeHTTP(w, r, next) +} + +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// tracing { +// [span ] +// } +// +func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + setParameter := func(d *caddyfile.Dispenser, val *string) error { + if d.NextArg() { + *val = d.Val() + } else { + return d.ArgErr() + } + if d.NextArg() { + return d.ArgErr() + } + return nil + } + + // paramsMap is a mapping between "string" parameter from the Caddyfile and its destination within the module + paramsMap := map[string]*string{ + "span": &ot.SpanName, + } + + for d.Next() { + args := d.RemainingArgs() + if len(args) > 0 { + return d.ArgErr() + } + + for d.NextBlock(0) { + if dst, ok := paramsMap[d.Val()]; ok { + if err := setParameter(d, dst); err != nil { + return err + } + } else { + return d.ArgErr() + } + } + } + return nil +} + +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var m Tracing + err := m.UnmarshalCaddyfile(h.Dispenser) + return &m, err +} + +// Interface guards +var ( + _ caddy.Provisioner = (*Tracing)(nil) + _ caddyhttp.MiddlewareHandler = (*Tracing)(nil) + _ caddyfile.Unmarshaler = (*Tracing)(nil) +) diff --git a/modules/caddyhttp/tracing/module_test.go b/modules/caddyhttp/tracing/module_test.go new file mode 100644 index 00000000..0fbc05bb --- /dev/null +++ b/modules/caddyhttp/tracing/module_test.go @@ -0,0 +1,182 @@ +package tracing + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func TestTracing_UnmarshalCaddyfile(t *testing.T) { + tests := []struct { + name string + spanName string + d *caddyfile.Dispenser + wantErr bool + }{ + { + name: "Full config", + spanName: "my-span", + d: caddyfile.NewTestDispenser(` +tracing { + span my-span +}`), + wantErr: false, + }, + { + name: "Only span name in the config", + spanName: "my-span", + d: caddyfile.NewTestDispenser(` +tracing { + span my-span +}`), + wantErr: false, + }, + { + name: "Empty config", + d: caddyfile.NewTestDispenser(` +tracing { +}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ot := &Tracing{} + if err := ot.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalCaddyfile() error = %v, wantErrType %v", err, tt.wantErr) + } + + if ot.SpanName != tt.spanName { + t.Errorf("UnmarshalCaddyfile() SpanName = %v, want SpanName %v", ot.SpanName, tt.spanName) + } + }) + } +} + +func TestTracing_UnmarshalCaddyfile_Error(t *testing.T) { + tests := []struct { + name string + d *caddyfile.Dispenser + wantErr bool + }{ + { + name: "Unknown parameter", + d: caddyfile.NewTestDispenser(` + tracing { + foo bar + }`), + wantErr: true, + }, + { + name: "Missed argument", + d: caddyfile.NewTestDispenser(` +tracing { + span +}`), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ot := &Tracing{} + if err := ot.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalCaddyfile() error = %v, wantErrType %v", err, tt.wantErr) + } + }) + } +} + +func TestTracing_ServeHTTP_Propagation_Without_Initial_Headers(t *testing.T) { + ot := &Tracing{ + SpanName: "mySpan", + } + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + w := httptest.NewRecorder() + + var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { + traceparent := request.Header.Get("Traceparent") + if traceparent == "" || strings.HasPrefix(traceparent, "00-00000000000000000000000000000000-0000000000000000") { + t.Errorf("Invalid traceparent: %v", traceparent) + } + + return nil + } + + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + if err := ot.Provision(ctx); err != nil { + t.Errorf("Provision error: %v", err) + t.FailNow() + } + + if err := ot.ServeHTTP(w, req, handler); err != nil { + t.Errorf("ServeHTTP error: %v", err) + } +} + +func TestTracing_ServeHTTP_Propagation_With_Initial_Headers(t *testing.T) { + ot := &Tracing{ + SpanName: "mySpan", + } + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + req.Header.Set("traceparent", "00-11111111111111111111111111111111-1111111111111111-01") + w := httptest.NewRecorder() + + var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { + traceparent := request.Header.Get("Traceparent") + if !strings.HasPrefix(traceparent, "00-11111111111111111111111111111111") { + t.Errorf("Invalid traceparent: %v", traceparent) + } + + return nil + } + + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + if err := ot.Provision(ctx); err != nil { + t.Errorf("Provision error: %v", err) + t.FailNow() + } + + if err := ot.ServeHTTP(w, req, handler); err != nil { + t.Errorf("ServeHTTP error: %v", err) + } +} + +func TestTracing_ServeHTTP_Next_Error(t *testing.T) { + ot := &Tracing{ + SpanName: "mySpan", + } + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + w := httptest.NewRecorder() + + expectErr := errors.New("test error") + + var handler caddyhttp.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) error { + return expectErr + } + + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + if err := ot.Provision(ctx); err != nil { + t.Errorf("Provision error: %v", err) + t.FailNow() + } + + if err := ot.ServeHTTP(w, req, handler); err == nil || !errors.Is(err, expectErr) { + t.Errorf("expected error, got: %v", err) + } +} diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go new file mode 100644 index 00000000..ce23944c --- /dev/null +++ b/modules/caddyhttp/tracing/tracer.go @@ -0,0 +1,108 @@ +package tracing + +import ( + "context" + "fmt" + "net/http" + + "github.com/caddyserver/caddy/v2" + + caddycmd "github.com/caddyserver/caddy/v2/cmd" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" + "go.uber.org/zap" +) + +const ( + webEngineName = "Caddy" + defaultSpanName = "handler" + nextCallCtxKey caddy.CtxKey = "nextCall" +) + +// nextCall store the next handler, and the error value return on calling it (if any) +type nextCall struct { + next caddyhttp.Handler + err error +} + +// openTelemetryWrapper is responsible for the tracing injection, extraction and propagation. +type openTelemetryWrapper struct { + propagators propagation.TextMapPropagator + + handler http.Handler + + spanName string +} + +// newOpenTelemetryWrapper is responsible for the openTelemetryWrapper initialization using provided configuration. +func newOpenTelemetryWrapper( + ctx context.Context, + spanName string, +) (openTelemetryWrapper, error) { + if spanName == "" { + spanName = defaultSpanName + } + + ot := openTelemetryWrapper{ + spanName: spanName, + } + + res, err := ot.newResource(webEngineName, caddycmd.CaddyVersion()) + if err != nil { + return ot, fmt.Errorf("creating resource error: %w", err) + } + + traceExporter, err := otlptracegrpc.New(ctx) + if err != nil { + return ot, fmt.Errorf("creating trace exporter error: %w", err) + } + + ot.propagators = propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) + + tracerProvider := globalTracerProvider.getTracerProvider( + sdktrace.WithBatcher(traceExporter), + sdktrace.WithResource(res), + ) + + ot.handler = otelhttp.NewHandler(http.HandlerFunc(ot.serveHTTP), ot.spanName, otelhttp.WithTracerProvider(tracerProvider), otelhttp.WithPropagators(ot.propagators)) + return ot, nil +} + +// serveHTTP injects a tracing context and call the next handler. +func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request) { + ot.propagators.Inject(r.Context(), propagation.HeaderCarrier(r.Header)) + next := r.Context().Value(nextCallCtxKey).(*nextCall) + next.err = next.next.ServeHTTP(w, r) +} + +// ServeHTTP propagates call to the by wrapped by `otelhttp` next handler. +func (ot *openTelemetryWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + n := &nextCall{ + next: next, + err: nil, + } + ot.handler.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), nextCallCtxKey, n))) + + return n.err +} + +// cleanup flush all remaining data and shutdown a tracerProvider +func (ot *openTelemetryWrapper) cleanup(logger *zap.Logger) error { + return globalTracerProvider.cleanupTracerProvider(logger) +} + +// newResource creates a resource that describe current handler instance and merge it with a default attributes value. +func (ot *openTelemetryWrapper) newResource( + webEngineName, + webEngineVersion string, +) (*resource.Resource, error) { + return resource.Merge(resource.Default(), resource.NewSchemaless( + semconv.WebEngineNameKey.String(webEngineName), + semconv.WebEngineVersionKey.String(webEngineVersion), + )) +} diff --git a/modules/caddyhttp/tracing/tracer_test.go b/modules/caddyhttp/tracing/tracer_test.go new file mode 100644 index 00000000..36a32ff4 --- /dev/null +++ b/modules/caddyhttp/tracing/tracer_test.go @@ -0,0 +1,27 @@ +package tracing + +import ( + "context" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestOpenTelemetryWrapper_newOpenTelemetryWrapper(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + var otw openTelemetryWrapper + var err error + + if otw, err = newOpenTelemetryWrapper(ctx, + "", + ); err != nil { + t.Errorf("newOpenTelemetryWrapper() error = %v", err) + t.FailNow() + } + + if otw.propagators == nil { + t.Errorf("Propagators should not be empty") + } +} diff --git a/modules/caddyhttp/tracing/tracerprovider.go b/modules/caddyhttp/tracing/tracerprovider.go new file mode 100644 index 00000000..035425ed --- /dev/null +++ b/modules/caddyhttp/tracing/tracerprovider.go @@ -0,0 +1,63 @@ +package tracing + +import ( + "context" + "fmt" + "sync" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/zap" +) + +// globalTracerProvider stores global tracer provider and is responsible for graceful shutdown when nobody is using it. +var globalTracerProvider = &tracerProvider{} + +type tracerProvider struct { + mu sync.Mutex + tracerProvider *sdktrace.TracerProvider + tracerProvidersCounter int +} + +// getTracerProvider create or return an existing global TracerProvider +func (t *tracerProvider) getTracerProvider(opts ...sdktrace.TracerProviderOption) *sdktrace.TracerProvider { + t.mu.Lock() + defer t.mu.Unlock() + + t.tracerProvidersCounter++ + + if t.tracerProvider == nil { + t.tracerProvider = sdktrace.NewTracerProvider( + opts..., + ) + } + + return t.tracerProvider +} + +// cleanupTracerProvider gracefully shutdown a TracerProvider +func (t *tracerProvider) cleanupTracerProvider(logger *zap.Logger) error { + t.mu.Lock() + defer t.mu.Unlock() + + if t.tracerProvidersCounter > 0 { + t.tracerProvidersCounter-- + } + + if t.tracerProvidersCounter == 0 { + if t.tracerProvider != nil { + // tracerProvider.ForceFlush SHOULD be invoked according to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#forceflush + if err := t.tracerProvider.ForceFlush(context.Background()); err != nil { + logger.Error("forcing flush", zap.Error(err)) + } + + // tracerProvider.Shutdown MUST be invoked according to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shutdown + if err := t.tracerProvider.Shutdown(context.Background()); err != nil { + return fmt.Errorf("tracerProvider shutdown error: %w", err) + } + } + + t.tracerProvider = nil + } + + return nil +} diff --git a/modules/caddyhttp/tracing/tracerprovider_test.go b/modules/caddyhttp/tracing/tracerprovider_test.go new file mode 100644 index 00000000..cb2e5936 --- /dev/null +++ b/modules/caddyhttp/tracing/tracerprovider_test.go @@ -0,0 +1,43 @@ +package tracing + +import ( + "testing" + + "go.uber.org/zap" +) + +func Test_tracersProvider_getTracerProvider(t *testing.T) { + tp := tracerProvider{} + + tp.getTracerProvider() + tp.getTracerProvider() + + if tp.tracerProvider == nil { + t.Errorf("There should be tracer provider") + } + + if tp.tracerProvidersCounter != 2 { + t.Errorf("Tracer providers counter should equal to 2") + } +} + +func Test_tracersProvider_cleanupTracerProvider(t *testing.T) { + tp := tracerProvider{} + + tp.getTracerProvider() + tp.getTracerProvider() + + err := tp.cleanupTracerProvider(zap.NewNop()) + + if err != nil { + t.Errorf("There should be no error: %v", err) + } + + if tp.tracerProvider == nil { + t.Errorf("There should be tracer provider") + } + + if tp.tracerProvidersCounter != 1 { + t.Errorf("Tracer providers counter should equal to 1") + } +}