From 2a0dae982499dec93e24ab0efd92e01a608568d3 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Tue, 24 Aug 2021 09:51:53 +0200 Subject: [PATCH] [apmazure] add blob storage (#1105) * add azure blob storage instrumentation --- docs/instrumenting.asciidoc | 37 ++++ docs/supported-tech.asciidoc | 8 + module/apmazure/blob.go | 180 ++++++++++++++++++++ module/apmazure/blob_test.go | 318 +++++++++++++++++++++++++++++++++++ module/apmazure/doc.go | 20 +++ module/apmazure/go.mod | 17 ++ module/apmazure/go.sum | 115 +++++++++++++ module/apmazure/storage.go | 116 +++++++++++++ scripts/Dockerfile-testing | 2 + spancontext.go | 2 +- 10 files changed, 814 insertions(+), 1 deletion(-) create mode 100644 module/apmazure/blob.go create mode 100644 module/apmazure/blob_test.go create mode 100644 module/apmazure/doc.go create mode 100644 module/apmazure/go.mod create mode 100644 module/apmazure/go.sum create mode 100644 module/apmazure/storage.go diff --git a/docs/instrumenting.asciidoc b/docs/instrumenting.asciidoc index 89b4aabdf..371fe79c3 100644 --- a/docs/instrumenting.asciidoc +++ b/docs/instrumenting.asciidoc @@ -30,6 +30,7 @@ that framework's API. The request context can be used for reporting <> * <> * <> +* <> [[builtin-modules-apmhttp]] ==== module/apmhttp @@ -904,3 +905,39 @@ func (s *server) handleRequest(w http.ResponseWriter, req *http.Request) { ... } ---- + +[[builtin-modules-apmazure]] +==== module/apmazure +Package apmazure provides a means of instrumenting the +https://github.com/Azure/azure-pipeline-go[Azure Pipeline Go] pipeline object, +so that Azure requests are reported as spans within the current transaction. + +To create spans for Azure requests, you should create a new pipeline using the +relevant service's `NewPipeline` function, like `azblob.NewPipeline`, then wrap +the `pipeline.Pipeline` with `apmazure.WrapPipeline`. The returned `Pipeline` +can be used as normal. + +The following services are supported: + +- Blob Storage + + +[source,go] +---- +import ( + "github.com/Azure/azure-storage-blob-go/azblob" + + "go.elastic.co/apm/module/apmazure" +) + +func main() { + p := azblob.NewPipeline(azblob.NewAnonymousCredential(), po) + p = apmazure.WrapPipeline(p) + u, err := url.Parse("https://my-account.blob.core.windows.net") + serviceURL := azblob.NewServiceURL(*u, p) + containerURL := serviceURL.NewContainerURL("mycontainer") + blobURL := containerURL.NewBlobURL("readme.txt") + // Use the blobURL to interact with Blob Storage + ... +} +---- diff --git a/docs/supported-tech.asciidoc b/docs/supported-tech.asciidoc index 027e0ccb6..bbcb64471 100644 --- a/docs/supported-tech.asciidoc +++ b/docs/supported-tech.asciidoc @@ -308,6 +308,14 @@ https://github.com/aws/aws-sdk-go[AWS SDK Go]. See <> for more information about AWS SDK Go instrumentation. +[float] +==== Azure Storage +We provide instrumentation for Azure Storage. This is usable with +github.com/Azure/azure-storage-blob-go/azblob[Azure Blob Storage]. + +See <> for more information +about Azure SDK Go instrumentation. + [float] [[supported-tech-messaging-systems]] === Messaging Systems diff --git a/module/apmazure/blob.go b/module/apmazure/blob.go new file mode 100644 index 000000000..266708296 --- /dev/null +++ b/module/apmazure/blob.go @@ -0,0 +1,180 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build go1.14 +// +build go1.14 + +package apmazure // import "go.elastic.co/apm/module/apmazure" + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/Azure/azure-pipeline-go/pipeline" +) + +type blobRPC struct { + accountName string + resourceName string + req pipeline.Request +} + +func (b *blobRPC) name() string { + return fmt.Sprintf("AzureBlob %s %s", b.operation(), b.resourceName) +} + +func (b *blobRPC) _type() string { + return "storage" +} + +func (b *blobRPC) subtype() string { + return "azureblob" +} + +func (b *blobRPC) storageAccountName() string { + return b.accountName +} + +func (b *blobRPC) resource() string { + return b.resourceName +} + +func (b *blobRPC) operation() string { + if b.req.Method == http.MethodDelete { + return "Delete" + } + q := b.req.URL.Query() + switch b.req.Method { + // From net/http documentation: + // For client requests, an empty string means GET. + case http.MethodGet, "": + return getOperation(q) + case http.MethodPost: + return postOperation(q) + case http.MethodHead: + return headOperation(q) + case http.MethodPut: + return putOperation(q, b.req.Header) + default: + return b.req.Method + } +} + +func getOperation(v url.Values) string { + restype := v.Get("restype") + comp := v.Get("comp") + if (restype == "" && comp == "") || comp == "blocklist" { + return "Download" + } + if restype == "container" && comp == "" { + return "GetProperties" + } + + switch comp { + case "metadata": + return "GetMetadata" + case "acl": + return "GetAcl" + case "list": + if restype == "container" { + return "ListBlobs" + } + return "ListContainers" + case "tags": + if v.Get("where") != "" { + return "FindTags" + } + return "GetTags" + default: + return "unknown operation" + } +} + +func postOperation(v url.Values) string { + comp := v.Get("comp") + switch comp { + case "batch": + return "Batch" + case "query": + return "Query" + case "userdelegationkey": + return "GetUserDelegationKey" + default: + return "unknown operation" + } + +} +func headOperation(v url.Values) string { + restype := v.Get("restype") + comp := v.Get("comp") + if restype == "" && comp == "" { + return "GetProperties" + } + if restype != "container" { + return "unknown operation" + } + switch comp { + case "metadata": + return "GetMetadata" + case "acl": + return "GetAcl" + default: + return "unknown operation" + } +} +func putOperation(v url.Values, h http.Header) string { + // header.Get canonicalizes the key, ie. x-ms-copy-source->X-Ms-Copy-Source. + // The headers used are all lowercase, so we access the map directly. + _, copySource := h["x-ms-copy-source"] + _, blobType := h["x-ms-blob-type"] + _, pageWrite := h["x-ms-page-write"] + restype := v.Get("restype") + comp := v.Get("comp") + if restype == "container" && comp == "acl" { + return "SetAcl" + } + + if comp == "" && !(copySource || blobType || pageWrite) { + return "Create" + } + if copySource { + return "Copy" + } + if blobType { + return "Upload" + } + if comp == "page" && pageWrite { + return "Clear" + } + + switch comp { + case "block", "blocklist", "page", "appendblock": + return "Upload" + case "copy": + return "Abort" + case "metadata": + return "SetMetadata" + case "lease", "snapshot", "undelete", "seal", "rename": + return strings.Title(comp) + case "properties", "tags", "tier", "expiry": + return "Set" + strings.Title(comp) + default: + return "unknown operation" + } +} diff --git a/module/apmazure/blob_test.go b/module/apmazure/blob_test.go new file mode 100644 index 000000000..eae40cc81 --- /dev/null +++ b/module/apmazure/blob_test.go @@ -0,0 +1,318 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build go1.14 +// +build go1.14 + +package apmazure + +import ( + "context" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/Azure/azure-pipeline-go/pipeline" + "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.elastic.co/apm/apmtest" +) + +func TestBlob(t *testing.T) { + po := azblob.PipelineOptions{ + HTTPSender: new(fakeSender), + } + p := azblob.NewPipeline(azblob.NewAnonymousCredential(), po) + p = WrapPipeline(p) + u, err := url.Parse("https://fakeaccnt.blob.core.windows.net") + require.NoError(t, err) + serviceURL := azblob.NewServiceURL(*u, p) + containerURL := serviceURL.NewContainerURL("mycontainer") + blobURL := containerURL.NewBlobURL("readme.txt") + + _, spans, errors := apmtest.WithTransaction(func(ctx context.Context) { + blobURL.GetTags(ctx, nil) + }) + require.Len(t, errors, 1) + require.Len(t, spans, 1) + span := spans[0] + + assert.Equal(t, "storage", span.Type) + assert.Equal(t, "AzureBlob GetTags mycontainer/readme.txt", span.Name) + assert.Equal(t, 403, span.Context.HTTP.StatusCode) + assert.Equal(t, "azureblob", span.Subtype) + assert.Equal(t, "GetTags", span.Action) + destination := span.Context.Destination + assert.Equal(t, "fakeaccnt.blob.core.windows.net", destination.Address) + assert.Equal(t, 443, destination.Port) + assert.Equal(t, "azureblob/fakeaccnt", destination.Service.Resource) +} + +func TestGetOperation(t *testing.T) { + tcs := []struct { + want string + values url.Values + }{ + // https://github.com/elastic/apm/blob/master/specs/agents/tracing-instrumentation-azure.md#determining-operations + { + want: "Download", + values: url.Values{}, + }, + { + want: "Download", + values: url.Values{"comp": []string{"blocklist"}}, + }, + { + want: "GetProperties", + values: url.Values{"restype": []string{"container"}}, + }, + { + want: "GetMetadata", + values: url.Values{"comp": []string{"metadata"}}, + }, + { + want: "GetAcl", + values: url.Values{"restype": []string{"container"}, "comp": []string{"acl"}}, + }, + { + want: "ListBlobs", + values: url.Values{"restype": []string{"container"}, "comp": []string{"list"}}, + }, + { + want: "ListContainers", + values: url.Values{"comp": []string{"list"}}, + }, + { + want: "GetTags", + values: url.Values{"comp": []string{"tags"}}, + }, + { + want: "FindTags", + values: url.Values{"comp": []string{"tags"}, "where": []string{"value"}}, + }, + } + + for _, tc := range tcs { + assert.Equal(t, tc.want, getOperation(tc.values)) + } +} + +func TestHeadOperation(t *testing.T) { + tcs := []struct { + want string + values url.Values + }{ + // https://github.com/elastic/apm/blob/master/specs/agents/tracing-instrumentation-azure.md#determining-operations + { + want: "GetProperties", + values: url.Values{}, + }, + { + want: "GetMetadata", + values: url.Values{"restype": []string{"container"}, "comp": []string{"metadata"}}, + }, + { + want: "GetAcl", + values: url.Values{"restype": []string{"container"}, "comp": []string{"acl"}}, + }, + } + + for _, tc := range tcs { + assert.Equal(t, tc.want, headOperation(tc.values)) + } +} + +func TestPostOperation(t *testing.T) { + tcs := []struct { + want string + values url.Values + }{ + // https://github.com/elastic/apm/blob/master/specs/agents/tracing-instrumentation-azure.md#determining-operations + { + want: "unknown operation", + values: url.Values{}, + }, + { + want: "Batch", + values: url.Values{"comp": []string{"batch"}}, + }, + { + want: "Query", + values: url.Values{"comp": []string{"query"}}, + }, + { + want: "GetUserDelegationKey", + values: url.Values{"comp": []string{"userdelegationkey"}}, + }, + } + + for _, tc := range tcs { + assert.Equal(t, tc.want, postOperation(tc.values)) + } +} + +func TestPutOperation(t *testing.T) { + tcs := []struct { + want string + values url.Values + header http.Header + }{ + // https://github.com/elastic/apm/blob/master/specs/agents/tracing-instrumentation-azure.md#determining-operations + { + want: "Copy", + header: http.Header{"x-ms-copy-source": []string{}}, + }, + { + want: "Copy", + header: http.Header{"x-ms-copy-source": []string{}}, + values: url.Values{"comp": []string{"block"}}, + }, + { + want: "Copy", + header: http.Header{"x-ms-copy-source": []string{}}, + values: url.Values{"comp": []string{"page"}}, + }, + { + want: "Copy", + header: http.Header{"x-ms-copy-source": []string{}}, + values: url.Values{"comp": []string{"incrementalcopy"}}, + }, + { + want: "Copy", + header: http.Header{"x-ms-copy-source": []string{}}, + values: url.Values{"comp": []string{"appendblock"}}, + }, + { + want: "Abort", + values: url.Values{"comp": []string{"copy"}}, + }, + { + want: "Upload", + header: http.Header{"x-ms-blob-type": []string{"BlockBlob"}}, + }, + { + want: "Upload", + values: url.Values{"comp": []string{"block"}}, + }, + { + want: "Upload", + values: url.Values{"comp": []string{"blocklist"}}, + }, + { + want: "Upload", + values: url.Values{"comp": []string{"page"}}, + }, + { + want: "Upload", + values: url.Values{"comp": []string{"appendblock"}}, + }, + { + want: "Create", + header: http.Header{}, + values: url.Values{}, + }, + { + want: "SetMetadata", + values: url.Values{"comp": []string{"metadata"}}, + }, + { + want: "SetAcl", + values: url.Values{"restype": []string{"container"}, "comp": []string{"acl"}}, + }, + { + want: "Lease", + values: url.Values{"comp": []string{"lease"}}, + }, + { + want: "Snapshot", + values: url.Values{"comp": []string{"snapshot"}}, + }, + { + want: "Undelete", + values: url.Values{"comp": []string{"undelete"}}, + }, + { + want: "Seal", + values: url.Values{"comp": []string{"seal"}}, + }, + { + want: "Rename", + values: url.Values{"comp": []string{"rename"}}, + }, + { + want: "SetProperties", + values: url.Values{"comp": []string{"properties"}}, + }, + { + want: "SetTags", + values: url.Values{"comp": []string{"tags"}}, + }, + { + want: "SetTier", + values: url.Values{"comp": []string{"tier"}}, + }, + { + want: "SetExpiry", + values: url.Values{"comp": []string{"expiry"}}, + }, + { + want: "Clear", + header: http.Header{"x-ms-page-write": []string{}}, + values: url.Values{"comp": []string{"page"}}, + }, + } + + for _, tc := range tcs { + assert.Equal(t, tc.want, putOperation(tc.values, tc.header)) + } +} + +type fakeSender struct{} + +func (s *fakeSender) New(next pipeline.Policy, po *pipeline.PolicyOptions) pipeline.Policy { + return s +} + +func (s *fakeSender) Do(ctx context.Context, request pipeline.Request) (pipeline.Response, error) { + resp := &http.Response{ + StatusCode: http.StatusForbidden, + Status: "403 Forbidden", + Body: &readCloser{strings.NewReader("")}, + Request: request.Request, + } + + return pipeline.NewHTTPResponse(resp), nil +} + +type readCloser struct { + io.Reader +} + +func (r *readCloser) Read(p []byte) (int, error) { + if r.Reader != nil { + return r.Reader.Read(p) + } + return len(p), nil +} + +func (r *readCloser) Close() error { + return nil +} diff --git a/module/apmazure/doc.go b/module/apmazure/doc.go new file mode 100644 index 000000000..5f4f82779 --- /dev/null +++ b/module/apmazure/doc.go @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Package apmazure provides tracing and error-reporting middleware for +// azure-sdk-for-go. +package apmazure // import "go.elastic.co/apm/module/apmazure" diff --git a/module/apmazure/go.mod b/module/apmazure/go.mod new file mode 100644 index 000000000..dbe1555c7 --- /dev/null +++ b/module/apmazure/go.mod @@ -0,0 +1,17 @@ +module go.elastic.co/apm/module/apmazure + +go 1.14 + +require ( + github.com/Azure/azure-pipeline-go v0.2.3 + github.com/Azure/azure-storage-blob-go v0.14.0 + github.com/stretchr/testify v1.7.0 + go.elastic.co/apm v1.13.1 + go.elastic.co/apm/module/apmhttp v1.13.1 + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect + golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect +) + +replace go.elastic.co/apm => ../.. + +replace go.elastic.co/apm/module/apmhttp => ../apmhttp diff --git a/module/apmazure/go.sum b/module/apmazure/go.sum new file mode 100644 index 000000000..8004a113e --- /dev/null +++ b/module/apmazure/go.sum @@ -0,0 +1,115 @@ +github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elastic/go-licenser v0.3.1 h1:RmRukU/JUmts+rpexAw0Fvt2ly7VVu6mw8z4HrEzObU= +github.com/elastic/go-licenser v0.3.1/go.mod h1:D8eNQk70FOCVBl3smCGQt/lv7meBeQno2eI1S5apiHQ= +github.com/elastic/go-sysinfo v1.1.1 h1:ZVlaLDyhVkDfjwPGU55CQRCRolNpc7P0BbyhhQZQmMI= +github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0= +github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= +github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jcchavezs/porto v0.1.0 h1:Xmxxn25zQMmgE7/yHYmh19KcItG81hIwfbEEFnd6w/Q= +github.com/jcchavezs/porto v0.1.0/go.mod h1:fESH0gzDHiutHRdX2hv27ojnOVFco37hg1W6E9EZF4A= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/prometheus/procfs v0.0.0-20190425082905-87a4384529e0 h1:c8R11WC8m7KNMkTv/0+Be8vvwo4I3/Ut9AC2FW8fX3U= +github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= +github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.elastic.co/fastjson v1.1.0 h1:3MrGBWWVIxe/xvsbpghtkFoPciPhOCmjsR/HfwEeQR4= +go.elastic.co/fastjson v1.1.0/go.mod h1:boNGISWMjQsUPy/t6yqt2/1Wx4YNPSe+mZjlyw9vKKI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5 h1:MeC2gMlMdkd67dn17MEby3rGXRxZtWeiRXOnISfTQ74= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= diff --git a/module/apmazure/storage.go b/module/apmazure/storage.go new file mode 100644 index 000000000..75a2deef3 --- /dev/null +++ b/module/apmazure/storage.go @@ -0,0 +1,116 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build go1.14 +// +build go1.14 + +package apmazure // import "go.elastic.co/apm/module/apmazure" + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-pipeline-go/pipeline" + + "go.elastic.co/apm" + "go.elastic.co/apm/module/apmhttp" + "go.elastic.co/apm/stacktrace" +) + +func init() { + stacktrace.RegisterLibraryPackage( + "github.com/Azure/azure-pipeline-go", + "github.com/Azure/azure-storage-blob-go/azblob", + ) +} + +// WrapPipeline wraps the provided pipeline.Pipeline, returning a new one that +// instruments requests and responses. +func WrapPipeline(p pipeline.Pipeline) pipeline.Pipeline { + return &apmPipeline{p} +} + +type apmPipeline struct { + next pipeline.Pipeline +} + +func (p *apmPipeline) Do( + ctx context.Context, + methodFactory pipeline.Factory, + req pipeline.Request, +) (pipeline.Response, error) { + rpc, err := newAzureRPC(req) + if err != nil { + return p.next.Do(ctx, methodFactory, req) + } + + span, ctx := apm.StartSpan(ctx, rpc.name(), rpc._type()) + defer span.End() + if !span.Dropped() { + ctx = apm.ContextWithSpan(ctx, span) + req.Request = apmhttp.RequestWithContext(ctx, req.Request) + span.Context.SetHTTPRequest(req.Request) + } else { + return p.next.Do(ctx, methodFactory, req) + } + span.Action = rpc.operation() + span.Subtype = rpc.subtype() + span.Context.SetDestinationService(apm.DestinationServiceSpanContext{ + Resource: rpc.subtype() + "/" + rpc.storageAccountName(), + }) + + resp, err := p.next.Do(ctx, methodFactory, req) + if err != nil { + apm.CaptureError(ctx, err).Send() + } + // We may still have a response even if err != nil + // eg., the client library considers 4XX as an error but still returns + // the response to us. + if resp.Response() != nil { + span.Context.SetHTTPStatusCode(resp.Response().StatusCode) + } + + return resp, err +} + +type azureRPC interface { + name() string + _type() string + subtype() string + storageAccountName() string + resource() string + operation() string +} + +func newAzureRPC(req pipeline.Request) (azureRPC, error) { + split := strings.Split(req.Host, ".") + accountName, storage := split[0], split[1] + var rpc azureRPC + if storage == "blob" { + rpc = &blobRPC{ + resourceName: strings.TrimPrefix(req.URL.Path, "/"), + accountName: accountName, + req: req, + } + } + if rpc == nil { + return nil, errors.New("unsupported service") + } + + return rpc, nil +} diff --git a/scripts/Dockerfile-testing b/scripts/Dockerfile-testing index 3b7affeea..95dd12760 100644 --- a/scripts/Dockerfile-testing +++ b/scripts/Dockerfile-testing @@ -6,6 +6,7 @@ COPY go.mod go.sum /go/src/go.elastic.co/apm/ COPY internal/apmgodog/go.mod internal/apmgodog/go.sum /go/src/go.elastic.co/apm/internal/apmgodog/ COPY internal/tracecontexttest/go.mod internal/tracecontexttest/go.sum /go/src/go.elastic.co/apm/internal/tracecontexttest/ COPY module/apmawssdkgo/go.mod module/apmawssdkgo/go.sum /go/src/go.elastic.co/apm/module/apmawssdkgo/ +COPY module/apmazure/go.mod module/apmazure/go.sum /go/src/go.elastic.co/apm/module/apmazure/ COPY module/apmbeego/go.mod module/apmbeego/go.sum /go/src/go.elastic.co/apm/module/apmbeego/ COPY module/apmchi/go.mod module/apmchi/go.sum /go/src/go.elastic.co/apm/module/apmchi/ COPY module/apmchiv5/go.mod module/apmchiv5/go.sum /go/src/go.elastic.co/apm/module/apmchiv5/ @@ -46,6 +47,7 @@ RUN cd /go/src/go.elastic.co/apm && go mod download RUN cd /go/src/go.elastic.co/apm/internal/apmgodog && go mod download RUN cd /go/src/go.elastic.co/apm/internal/tracecontexttest && go mod download RUN cd /go/src/go.elastic.co/apm/module/apmawssdkgo && go mod download +RUN cd /go/src/go.elastic.co/apm/module/apmazure && go mod download RUN cd /go/src/go.elastic.co/apm/module/apmbeego && go mod download RUN cd /go/src/go.elastic.co/apm/module/apmchi && go mod download RUN cd /go/src/go.elastic.co/apm/module/apmchiv5 && go mod download diff --git a/spancontext.go b/spancontext.go index 8503fdd54..a1eb2f8dd 100644 --- a/spancontext.go +++ b/spancontext.go @@ -199,7 +199,7 @@ func (c *SpanContext) SetHTTPStatusCode(statusCode int) { // SetDestinationAddress sets the destination address and port in the context. // -// SetDestinationAddress has no effect when called when an empty addr. +// SetDestinationAddress has no effect when called with an empty addr. func (c *SpanContext) SetDestinationAddress(addr string, port int) { if addr != "" { c.destination.Address = truncateString(addr)