From 1084ecac116ebb2e2e5b43b81b33b6f134643e58 Mon Sep 17 00:00:00 2001 From: beegiik Date: Fri, 13 Jun 2025 12:13:51 +0100 Subject: [PATCH 1/9] feat: create new telemetry handle with connection strings --- aitelemetry/telemetrywrapper.go | 38 +++++++++++++++++++++++++++- aitelemetry/telemetrywrapper_test.go | 27 ++++++++++++++++---- go.mod | 1 + go.sum | 9 +++++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/aitelemetry/telemetrywrapper.go b/aitelemetry/telemetrywrapper.go index 39deebb802..ba4362ad97 100644 --- a/aitelemetry/telemetrywrapper.go +++ b/aitelemetry/telemetrywrapper.go @@ -161,7 +161,7 @@ func isPublicEnvironment(url string, retryCount, waitTimeInSecs int) (bool, erro return true, nil } else if err == nil { debugLog("[AppInsights] This is not azure public cloud:%s", cloudName) - return false, fmt.Errorf("Not an azure public cloud: %s", cloudName) + return false, fmt.Errorf("not an azure public cloud: %s", cloudName) } debugLog("GetAzureCloud returned err :%v", err) @@ -214,6 +214,42 @@ func NewAITelemetry( return th, nil } +// NewAITelemetry creates telemetry handle with user specified appinsights connection string. +func NewAITelemetryWithConnectionString( + cString string, + aiConfig AIConfig, +) (TelemetryHandle, error) { + debugMode = aiConfig.DebugMode + + if cString == "" { + debugLog("Empty connection string") + return nil, fmt.Errorf("AI connection string is empty") + } + + setAIConfigDefaults(&aiConfig) + + telemetryConfig := appinsights.NewTelemetryConfigurationWithConnectionString(cString) + telemetryConfig.MaxBatchSize = aiConfig.BatchSize + telemetryConfig.MaxBatchInterval = time.Duration(aiConfig.BatchInterval) * time.Second + + th := &telemetryHandle{ + client: appinsights.NewTelemetryClientFromConfig(telemetryConfig), + appName: aiConfig.AppName, + appVersion: aiConfig.AppVersion, + diagListener: messageListener(), + disableMetadataRefreshThread: aiConfig.DisableMetadataRefreshThread, + refreshTimeout: aiConfig.RefreshTimeout, + } + + if th.disableMetadataRefreshThread { + getMetadata(th) + } else { + go getMetadata(th) + } + + return th, nil +} + // TrackLog function sends report (trace) to appinsights resource. It overrides few of the existing columns with app information // and for rest it uses custom dimesion func (th *telemetryHandle) TrackLog(report Report) { diff --git a/aitelemetry/telemetrywrapper_test.go b/aitelemetry/telemetrywrapper_test.go index 2ccb175217..774e3ef26e 100644 --- a/aitelemetry/telemetrywrapper_test.go +++ b/aitelemetry/telemetrywrapper_test.go @@ -19,6 +19,7 @@ var ( hostAgentUrl = "localhost:3501" getCloudResponse = "AzurePublicCloud" httpURL = "http://" + hostAgentUrl + connectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" ) func TestMain(m *testing.M) { @@ -89,7 +90,12 @@ func TestEmptyAIKey(t *testing.T) { } _, err = NewAITelemetry(httpURL, "", aiConfig) if err == nil { - t.Errorf("Error intializing AI telemetry:%v", err) + t.Errorf("Error initializing AI telemetry:%v", err) + } + + _, err = NewAITelemetryWithConnectionString("", aiConfig) + if err == nil { + t.Errorf("Error initializing AI telemetry with connection string:%v", err) } } @@ -107,9 +113,14 @@ func TestNewAITelemetry(t *testing.T) { DebugMode: true, DisableMetadataRefreshThread: true, } - th, err = NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) - if th == nil { - t.Errorf("Error intializing AI telemetry: %v", err) + th1, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) + if th1 == nil { + t.Errorf("Error initializing AI telemetry: %v", err) + } + + th2, err := NewAITelemetryWithConnectionString(connectionString, aiConfig) + if th2 == nil { + t.Errorf("Error initializing AI telemetry with connection string: %v", err) } } @@ -171,8 +182,14 @@ func TestClosewithoutSend(t *testing.T) { thtest, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) if thtest == nil { - t.Errorf("Error intializing AI telemetry:%v", err) + t.Errorf("Error initializing AI telemetry:%v", err) + } + + thtest2, err := NewAITelemetryWithConnectionString(connectionString, aiConfig) + if thtest2 == nil { + t.Errorf("Error initializing AI telemetry with connection string:%v", err) } thtest.Close(10) + thtest2.Close(10) } diff --git a/go.mod b/go.mod index beafd879a3..ebf3cbf9a8 100644 --- a/go.mod +++ b/go.mod @@ -250,6 +250,7 @@ require ( ) replace ( + github.com/microsoft/ApplicationInsights-Go => github.com/beegiik/ApplicationInsights-Go v1.8.0 github.com/onsi/ginkgo => github.com/onsi/ginkgo v1.12.0 github.com/onsi/gomega => github.com/onsi/gomega v1.10.0 ) diff --git a/go.sum b/go.sum index ec544070f7..a9b15a0df3 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu github.com/avast/retry-go/v3 v3.1.1/go.mod h1:6cXRK369RpzFL3UQGqIUp9Q7GDrams+KsYWrfNA1/nQ= github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/beegiik/ApplicationInsights-Go v1.8.0 h1:9eQ7wk7o03GA7HM/oDSOf/STOq5YA09cJfUiZa0yobU= +github.com/beegiik/ApplicationInsights-Go v1.8.0/go.mod h1:wGv9tvjn4hfY0O95MzNM7ftYfphBi0BGqknkBdFF/cM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/billgraziano/dpapi v0.5.0 h1:pcxA17vyjbDqYuxCFZbgL9tYIk2xgbRZjRaIbATwh+8= @@ -348,6 +350,7 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +<<<<<<< HEAD github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= @@ -356,6 +359,8 @@ github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81T github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +======= +>>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -471,7 +476,10 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +<<<<<<< HEAD github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +======= +>>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -481,6 +489,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= From 6a0680fc81397b77766f33546b719218b5e6f2d2 Mon Sep 17 00:00:00 2001 From: beegiik Date: Thu, 3 Jul 2025 15:27:12 +0100 Subject: [PATCH 2/9] feat: Add application insights source code and remove dependency --- application-insights/.gitignore | 31 + application-insights/.travis.yml | 15 + application-insights/CODE_OF_CONDUCT.md | 1 + application-insights/CONTRIBUTING.md | 4 + application-insights/LICENSE | 22 + application-insights/PULL_REQUEST_TEMPLATE.md | 8 + application-insights/README.md | 557 +++++++++++++++ application-insights/SECURITY.md | 41 ++ application-insights/appinsights/client.go | 155 +++++ .../appinsights/client_test.go | 236 +++++++ application-insights/appinsights/clock.go | 11 + .../appinsights/clock_test.go | 39 ++ .../appinsights/configuration.go | 68 ++ .../appinsights/configuration_test.go | 38 + .../appinsights/connection_string_parser.go | 39 ++ .../connection_string_parser_test.go | 66 ++ application-insights/appinsights/constants.go | 20 + .../appinsights/contracts/availabilitydata.go | 111 +++ .../appinsights/contracts/base.go | 25 + .../appinsights/contracts/contexttagkeys.go | 153 ++++ .../appinsights/contracts/contexttags.go | 565 +++++++++++++++ .../appinsights/contracts/data.go | 25 + .../appinsights/contracts/datapoint.go | 54 ++ .../appinsights/contracts/datapointtype.go | 22 + .../appinsights/contracts/domain.go | 21 + .../appinsights/contracts/envelope.go | 82 +++ .../appinsights/contracts/eventdata.go | 82 +++ .../appinsights/contracts/exceptiondata.go | 93 +++ .../appinsights/contracts/exceptiondetails.go | 66 ++ .../appinsights/contracts/messagedata.go | 72 ++ .../appinsights/contracts/metricdata.go | 68 ++ .../appinsights/contracts/package.go | 4 + .../appinsights/contracts/pageviewdata.go | 85 +++ .../contracts/remotedependencydata.go | 134 ++++ .../appinsights/contracts/requestdata.go | 125 ++++ .../appinsights/contracts/severitylevel.go | 31 + .../appinsights/contracts/stackframe.go | 52 ++ .../appinsights/diagnostics.go | 88 +++ .../appinsights/diagnostics_test.go | 120 ++++ application-insights/appinsights/exception.go | 150 ++++ .../appinsights/exception_test.go | 193 ++++++ .../appinsights/inmemorychannel.go | 449 ++++++++++++ .../appinsights/inmemorychannel_test.go | 628 +++++++++++++++++ .../appinsights/jsonserializer.go | 25 + .../appinsights/jsonserializer_test.go | 483 +++++++++++++ application-insights/appinsights/package.go | 8 + application-insights/appinsights/telemetry.go | 652 ++++++++++++++++++ .../appinsights/telemetry_test.go | 368 ++++++++++ .../appinsights/telemetrychannel.go | 51 ++ .../appinsights/telemetrycontext.go | 104 +++ .../appinsights/telemetrycontext_test.go | 145 ++++ application-insights/appinsights/throttle.go | 144 ++++ .../appinsights/transmitter.go | 240 +++++++ .../appinsights/transmitter_test.go | 508 ++++++++++++++ application-insights/appinsights/uuid.go | 72 ++ application-insights/appinsights/uuid_test.go | 65 ++ application-insights/generateschema.ps1 | 92 +++ application-insights/go.mod | 15 + application-insights/go.sum | 38 + go.mod | 14 +- go.sum | 35 +- 61 files changed, 7890 insertions(+), 18 deletions(-) create mode 100644 application-insights/.gitignore create mode 100644 application-insights/.travis.yml create mode 100644 application-insights/CODE_OF_CONDUCT.md create mode 100644 application-insights/CONTRIBUTING.md create mode 100644 application-insights/LICENSE create mode 100644 application-insights/PULL_REQUEST_TEMPLATE.md create mode 100644 application-insights/README.md create mode 100644 application-insights/SECURITY.md create mode 100644 application-insights/appinsights/client.go create mode 100644 application-insights/appinsights/client_test.go create mode 100644 application-insights/appinsights/clock.go create mode 100644 application-insights/appinsights/clock_test.go create mode 100644 application-insights/appinsights/configuration.go create mode 100644 application-insights/appinsights/configuration_test.go create mode 100644 application-insights/appinsights/connection_string_parser.go create mode 100644 application-insights/appinsights/connection_string_parser_test.go create mode 100644 application-insights/appinsights/constants.go create mode 100644 application-insights/appinsights/contracts/availabilitydata.go create mode 100644 application-insights/appinsights/contracts/base.go create mode 100644 application-insights/appinsights/contracts/contexttagkeys.go create mode 100644 application-insights/appinsights/contracts/contexttags.go create mode 100644 application-insights/appinsights/contracts/data.go create mode 100644 application-insights/appinsights/contracts/datapoint.go create mode 100644 application-insights/appinsights/contracts/datapointtype.go create mode 100644 application-insights/appinsights/contracts/domain.go create mode 100644 application-insights/appinsights/contracts/envelope.go create mode 100644 application-insights/appinsights/contracts/eventdata.go create mode 100644 application-insights/appinsights/contracts/exceptiondata.go create mode 100644 application-insights/appinsights/contracts/exceptiondetails.go create mode 100644 application-insights/appinsights/contracts/messagedata.go create mode 100644 application-insights/appinsights/contracts/metricdata.go create mode 100644 application-insights/appinsights/contracts/package.go create mode 100644 application-insights/appinsights/contracts/pageviewdata.go create mode 100644 application-insights/appinsights/contracts/remotedependencydata.go create mode 100644 application-insights/appinsights/contracts/requestdata.go create mode 100644 application-insights/appinsights/contracts/severitylevel.go create mode 100644 application-insights/appinsights/contracts/stackframe.go create mode 100644 application-insights/appinsights/diagnostics.go create mode 100644 application-insights/appinsights/diagnostics_test.go create mode 100644 application-insights/appinsights/exception.go create mode 100644 application-insights/appinsights/exception_test.go create mode 100644 application-insights/appinsights/inmemorychannel.go create mode 100644 application-insights/appinsights/inmemorychannel_test.go create mode 100644 application-insights/appinsights/jsonserializer.go create mode 100644 application-insights/appinsights/jsonserializer_test.go create mode 100644 application-insights/appinsights/package.go create mode 100644 application-insights/appinsights/telemetry.go create mode 100644 application-insights/appinsights/telemetry_test.go create mode 100644 application-insights/appinsights/telemetrychannel.go create mode 100644 application-insights/appinsights/telemetrycontext.go create mode 100644 application-insights/appinsights/telemetrycontext_test.go create mode 100644 application-insights/appinsights/throttle.go create mode 100644 application-insights/appinsights/transmitter.go create mode 100644 application-insights/appinsights/transmitter_test.go create mode 100644 application-insights/appinsights/uuid.go create mode 100644 application-insights/appinsights/uuid_test.go create mode 100644 application-insights/generateschema.ps1 create mode 100644 application-insights/go.mod create mode 100644 application-insights/go.sum diff --git a/application-insights/.gitignore b/application-insights/.gitignore new file mode 100644 index 0000000000..69d352625b --- /dev/null +++ b/application-insights/.gitignore @@ -0,0 +1,31 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test +.idea + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# OS specific +.DS_Store + +# Backup filese +*~ diff --git a/application-insights/.travis.yml b/application-insights/.travis.yml new file mode 100644 index 0000000000..26a63095a0 --- /dev/null +++ b/application-insights/.travis.yml @@ -0,0 +1,15 @@ +language: go + +go: + - 1.13.x + - 1.14.x + - 1.15.x + +sudo: false + +os: + - osx + - linux + +script: + - go test -v ./appinsights/... diff --git a/application-insights/CODE_OF_CONDUCT.md b/application-insights/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..65c8a42b8d --- /dev/null +++ b/application-insights/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/application-insights/CONTRIBUTING.md b/application-insights/CONTRIBUTING.md new file mode 100644 index 0000000000..dfa775d0a4 --- /dev/null +++ b/application-insights/CONTRIBUTING.md @@ -0,0 +1,4 @@ +# How to Contribute + +If you're interested in contributing, take a look at the general [contributer's guide](https://github.com/microsoft/ApplicationInsights-Home/blob/master/CONTRIBUTING.md) first. + diff --git a/application-insights/LICENSE b/application-insights/LICENSE new file mode 100644 index 0000000000..01d022c227 --- /dev/null +++ b/application-insights/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015-2017 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/application-insights/PULL_REQUEST_TEMPLATE.md b/application-insights/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..6b379938de --- /dev/null +++ b/application-insights/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Fix # . + + +For significant contributions please make sure you have completed the following items: + +- [ ] Design discussion issue # +- [ ] Changes in public surface reviewed +- [ ] CHANGELOG.md updated diff --git a/application-insights/README.md b/application-insights/README.md new file mode 100644 index 0000000000..fb9ec325c4 --- /dev/null +++ b/application-insights/README.md @@ -0,0 +1,557 @@ +# Microsoft Application Insights SDK for Go + +This project provides a Go SDK for Application Insights. +[Application Insights](http://azure.microsoft.com/en-us/services/application-insights/) +is a service that allows developers to keep their applications available, +performant, and successful. This go package will allow you to send +telemetry of various kinds (event, metric, trace) to the Application +Insights service where they can be visualized in the Azure Portal. + +## Status +This SDK is NOT currently maintained or supported, by Azure Container Networking or Microsoft. Azure Monitor only provides support when using our [supported SDKs](https://docs.microsoft.com/en-us/azure/azure-monitor/app/platforms#unsupported-community-sdks), and this SDK does not yet meet that standard. + +Known gaps include: +* Operation correlation is not supported, but this can be managed by the + caller through the interfaces that exist today. +* Sampling is not supported. The more mature SDKs support dynamic sampling, + but at present this does not even support manual sampling. +* Automatic collection of events is not supported. All telemetry must be + explicitly collected and sent by the user. +* Offline storage of telemetry is not supported. The .Net SDK is capable of + spilling events to disk in case of network interruption. This SDK has no + such feature. + +We’re constantly assessing opportunities to expand our support for other languages, so follow our [Azure Updates](https://azure.microsoft.com/updates/?query=application%20insights) page to receive the latest SDK news. + +## Requirements +**Install** +``` +go get github.com/microsoft/ApplicationInsights-Go/appinsights +``` +**Get an instrumentation key** +>**Note**: an instrumentation key is required before any data can be sent. Please see the "[Getting an Application Insights Instrumentation Key](https://github.com/microsoft/AppInsights-Home/wiki#getting-an-application-insights-instrumentation-key)" section of the wiki for more information. To try the SDK without an instrumentation key, set the instrumentationKey config value to a non-empty string. + +# Usage + +## Setup + +To start tracking telemetry, you'll want to first initialize a +[telemetry client](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryClient). + +```go +import "github.com/microsoft/ApplicationInsights-Go/appinsights" + +func main() { + client := appinsights.NewTelemetryClient("") +} +``` + +If you want more control over the client's behavior, you should initialize a +new [TelemetryConfiguration](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryConfiguration) +object and use it to create a client: + +```go +import "time" +import "github.com/microsoft/ApplicationInsights-Go/appinsights" + +func main() { + telemetryConfig := appinsights.NewTelemetryConfiguration("") + + // Configure how many items can be sent in one call to the data collector: + telemetryConfig.MaxBatchSize = 8192 + + // Configure the maximum delay before sending queued telemetry: + telemetryConfig.MaxBatchInterval = 2 * time.Second + + client := appinsights.NewTelemetryClientFromConfig(telemetryConfig) +} +``` + +This client will be used to submit all of your telemetry to Application +Insights. This SDK does not presently collect any telemetry automatically, +so you will use this client extensively to report application health and +status. You may want to store it in a global variable or otherwise include +it in your data model. + +## Telemetry submission + +The [TelemetryClient](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryClient) +itself has several methods for submitting telemetry: + +```go +type TelemetryClient interface { + // (much omitted) + + // Log a user action with the specified name + TrackEvent(name string) + + // Log a numeric value that is not specified with a specific event. + // Typically used to send regular reports of performance indicators. + TrackMetric(name string, value float64) + + // Log a trace message with the specified severity level. + TrackTrace(name string, severity contracts.SeverityLevel) + + // Log an HTTP request with the specified method, URL, duration and + // response code. + TrackRequest(method, url string, duration time.Duration, responseCode string) + + // Log a dependency with the specified name, type, target, and + // success status. + TrackRemoteDependency(name, dependencyType, target string, success bool) + + // Log an availability test result with the specified test name, + // duration, and success status. + TrackAvailability(name string, duration time.Duration, success bool) + + // Log an exception with the specified error, which may be a string, + // error or Stringer. The current callstack is collected + // automatically. + TrackException(err interface{}) +} +``` + +These may be used directly to log basic telemetry a manner you might expect: + +```go +client.TrackMetric("Queue Length", len(queue)) + +client.TrackEvent("Client connected") +``` + +But the inputs to these methods only capture the very basics of what these +telemetry types can represent. For example, all telemetry supports custom +properties, which are inaccessible through the above methods. More complete +versions are available through use of *telemetry item* classes, which can +then be submitted through the `TelemetryClient.Track` method, as illustrated +in the below sections: + +### Trace +[Trace telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TraceTelemetry) +represent printf-like trace statements that can be text searched. They have +an associated severity level, values for which are found in the package's +constants: + +```go +const ( + Verbose contracts.SeverityLevel = contracts.Verbose + Information contracts.SeverityLevel = contracts.Information + Warning contracts.SeverityLevel = contracts.Warning + Error contracts.SeverityLevel = contracts.Error + Critical contracts.SeverityLevel = contracts.Critical +) +``` + +Trace telemetry is fairly simple, but common telemetry properties are also +available: + +```go +trace := appinsights.NewTraceTelemetry("message", appinsights.Warning) + +// You can set custom properties on traces +trace.Properties["module"] = "server" + +// You can also fudge the timestamp: +trace.Timestamp = time.Now().Sub(time.Minute) + +// Finally, track it +client.Track(trace) +``` + +### Events +[Event telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#EventTelemetry) +represent structured event records. + +```go +event := appinsights.NewEventTelemetry("button clicked") +event.Properties["property"] = "value" +client.Track(event) +``` + +### Single-value metrics +[Metric telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#MetricTelemetry) +each represent a single data point. + +```go +metric := appinsights.NewMetricTelemetry("Queue length", len(q.items)) +metric.Properties["Queue name"] = q.name +client.Track(metric) +``` + +### Pre-aggregated metrics +To reduce the number of metric values that may be sent through telemetry, +when using a particularly high volume of measurements, metric data can be +[pre-aggregated by the client](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#AggregateMetricTelemetry) +and submitted all at once. + +```go +aggregate := appinsights.NewAggregateMetricTelemetry("metric name") + +var dataPoints []float64 +// ...collect data points... + +// If the data is sampled, then one should use the AddSampledData method to +// feed data to this telemetry type. +aggregate.AddSampledData(dataPoints) + +// If the entire population of data points is known, then one should instead +// use the AddData method. The difference between the two is the manner in +// which the standard deviation is calculated. +aggregate.AddData(dataPoints) + +// Alternatively, you can aggregate the data yourself and supply it to this +// telemetry item: +aggregate.Value = sum(dataPoints) +aggregate.Min = min(dataPoints) +aggregate.Max = max(dataPoints) +aggregate.Count = len(dataPoints) +aggregate.StdDev = stdDev(dataPoints) + +// Custom properties could be further added here... + +// Finally, track it: +client.Track(aggregate) +``` + +### Requests +[Request telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#RequestTelemetry) +represent completion of an external request to the application and contains +a summary of that request execution and results. This SDK's request +telemetry is focused on HTTP requests. + +```go +request := appinsights.NewRequestTelemetry("GET", "https://microsoft.com/", duration, "") + +// Note that the timestamp will be set to time.Now() minus the +// specified duration. This can be overridden by either manually +// setting the Timestamp and Duration fields, or with MarkTime: +request.MarkTime(requestStartTime, requestEndTime) + +// Source of request +request.Source = clientAddress + +// Success is normally inferred from the responseCode, but can be overridden: +request.Success = false + +// Request ID's are randomly generated GUIDs, but this can also be overridden: +request.Id = "" + +// Custom properties and measurements can be set here +request.Properties["user-agent"] = request.headers["User-agent"] +request.Measurements["POST size"] = float64(len(data)) + +// Context tags become more useful here as well +request.Tags.Session().SetId("") +request.Tags.User().SetAccountId("") + +// Finally track it +client.Track(request) +``` + +### Dependencies +[Remote dependency telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#RemoteDependencyTelemetry) +represent interactions of the monitored component with a remote +component/service like SQL or an HTTP endpoint. + +```go +dependency := appinsights.NewRemoteDependencyTelemetry("Redis cache", "Redis", "", true /* success */) + +// The result code is typically an error code or response status code +dependency.ResultCode = "OK" + +// Id's can be used for correlation if the remote end is also logging +// telemetry through application insights. +dependency.Id = "" + +// Data may contain the exact URL hit or SQL statements +dependency.Data = "MGET " + +// The duration can be set directly: +dependency.Duration = time.Minute +// or via MarkTime: +dependency.MarkTime(startTime, endTime) + +// Properties and measurements may be set. +dependency.Properties["shard-instance"] = "" +dependency.Measurements["data received"] = float64(len(response.data)) + +// Submit the telemetry +client.Track(dependency) +``` + +### Exceptions +[Exception telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#ExceptionTelemetry) +represent handled or unhandled exceptions that occurred during the execution +of the monitored application. This SDK is geared towards handling panics or +unexpected results from important functions: + +To handle a panic: + +```go +func method(client appinsights.TelemetryClient) { + defer func() { + if r := recover(); r != nil { + // Track the panic + client.TrackException(r) + + // Optionally, you may want to re-throw the panic: + panic(r) + } + }() + + // Panics in any code below will be handled by the above. + panic("AHHHH!!") +} +``` + +This can be condensed with a helper function: + +```go +func method(client appinsights.TelemetryClient) { + // false indicates that we should have this handle the panic, and + // not re-throw it. + defer appinsights.TrackPanic(client, false) + + // Panics in any code below will be handled by the above. + panic("AHHHH!!") +} +``` + +This will capture and report the call stack of the panic, including the site +of the function that handled the panic. Do note that Go does not unwind the +callstack while processing panics, so the trace will include any functions +that may be called by `method` in the example above leading up to the panic. + +This SDK will handle panic messages that are any of the types: `string`, +`error`, or anything that implements [fmt.Stringer](https://golang.org/pkg/fmt/#Stringer) +or [fmt.GoStringer](https://golang.org/pkg/fmt/#GoStringer). + +While the above example uses `client.TrackException`, you can also use the +longer form as in earlier examples -- and not only for panics: + +```go +value, err := someMethod(argument) +if err != nil { + exception := appinsights.NewExceptionTelemetry(err) + + // Set the severity level -- perhaps this isn't a critical + // issue, but we'd *really rather* it didn't fail: + exception.SeverityLevel = appinsights.Warning + + // One could tweak the number of stack frames to skip by + // reassigning the callstack -- for instance, if you were to + // log this exception in a helper method. + exception.Frames = appinsights.GetCallstack(3 /* frames to skip */) + + // Properties are available as usual + exception.Properties["input"] = argument + + // Track the exception + client.Track(exception) +} +``` + +### Availability +[Availability telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#AvailabilityTelemetry) +represent the result of executing an availability test. This is useful if +you are writing availability monitors in Go. + +```go +availability := appinsights.NewAvailabilityTelemetry("test name", callDuration, true /* success */) + +// The run location indicates where the test was run from +availability.RunLocation = "Phoenix" + +// Diagnostics message +availability.Message = diagnostics + +// Id is used for correlation with the target service +availability.Id = requestId + +// Timestamp and duration can be changed through MarkTime, similar +// to other telemetry types with Duration's +availability.MarkTime(testStartTime, testEndTime) + +// Submit the telemetry +client.Track(availability) +``` + +### Page Views +[Page view telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#PageViewTelemetry) +represent generic actions on a page like a button click. These are typically +generated by the client side rather than the server side, but is available +here nonetheless. + +```go +pageview := appinsights.NewPageViewTelemetry("Event name", "http://testuri.org/page") + +// A duration is available here. +pageview.Duration = time.Minute + +// As are the usual Properties and Measurements... + +// Track +client.Track(pageview) +``` + +### Context tags +Telemetry items all have a `Tags` property that contains information *about* +the submitted telemetry, such as user, session, and device information. The +`Tags` property is an instance of the +[contracts.ContextTags](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/contracts/#ContextTags) +type, which is a `map[string]string` under the hood, but has helper methods +to access the most commonly used data. An instance of +[TelemetryContext](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#TelemetryContext) +exists on the `TelemetryClient`, and also contains a `Tags` property. These +tags are applied to all telemetry sent through the client. If a context tag +is found on both the client's `TelemetryContext` and in the telemetry item's +`Tags`, the value associated with the telemtry takes precedence. + +A few examples for illustration: + +```go +import ( + "os" + + "github.com/microsoft/ApplicationInsights-Go/appinsights" + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +func main() { + client := appinsights.NewTelemetryClient("") + + // Set role instance name globally -- this is usually the + // name of the service submitting the telemetry + client.Context().Tags.Cloud().SetRole("my_go_server") + + // Set the role instance to the host name. Note that this is + // done automatically by the SDK. + client.Context().Tags.Cloud().SetRoleInstance(os.Hostname()) + + // Make a request to fiddle with the telemetry's context + req := appinsights.NewRequestTelemetry("GET", "http://server/path", time.Millisecond, "200") + + // Set the account ID context tag, for this telemetry item + // only. The following are equivalent: + req.Tags.User().SetAccountId("") + req.Tags[contracts.UserAccountId] = "" + + // This request will have all context tags above. + client.Track(req) +} +``` + +### Common properties + +In the same way that context tags can be written to all telemetry items, the +`TelemetryContext` has a `CommonProperties` map. Entries in this map will +be added to all telemetry items' custom properties (unless a telemetry item +already has that property set -- the telemetry item always has precedence). +This is useful for contextual data that may not be captured in the context +tags, for instance cluster identifiers or resource groups. + +```go +func main() { + client := appinsights.NewTelemetryClient("") + + client.Context().CommonProperties["Resource group"] = "My resource group" + // ... +} +``` + +### Shutdown +The Go SDK submits data asynchronously. The [InMemoryChannel](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#InMemoryChannel) +launches its own goroutine used to accept and send telemetry. If you're not +careful, this may result in lost telemetry when the service needs to shut +down. The channel has a few methods to deal with this case: + +* `Flush` will trigger telemetry submission for buffered items. It returns + immediately and telemetry is not guaranteed to have been sent. +* `Stop` will immediately shut down the channel and discard any unsubmitted + telemetry. Useful if you need to exit NOW. +* `Close` will cause the channel to stop accepting new telemetry, submit any + pending telemetry, and returns a channel that closes when the telemetry + buffer is fully empty. If telemetry submission fails, then `Close` will + retry until the specified duration elapses. If no duration is specified, + then it will give up if any telemetry submission fails. + +If at all possible, you should use `Close`: + +```go +func main() { + client := appinsights.NewTelemetryClient("") + + // ... run the service ... + + // on shutdown: + + select { + case <-client.Channel().Close(10 * time.Second): + // Ten second timeout for retries. + + // If we got here, then all telemetry was submitted + // successfully, and we can proceed to exiting. + case <-time.After(30 * time.Second): + // Thirty second absolute timeout. This covers any + // previous telemetry submission that may not have + // completed before Close was called. + + // There are a number of reasons we could have + // reached here. We gave it a go, but telemetry + // submission failed somewhere. Perhaps old events + // were still retrying, or perhaps we're throttled. + // Either way, we don't want to wait around for it + // to complete, so let's just exit. + } +} +``` + +We recommend something similar to the above to minimize lost telemetry +through shutdown. +[The documentation](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryChannel) +explains in more detail what can lead to the cases above. + +### Diagnostics +If you find yourself missing some of the telemetry that you thought was +submitted, diagnostics can be turned on to help troubleshoot problems with +telemetry submission. + +```go +appinsights.NewDiagnosticsMessageListener(func(msg string) error { + fmt.Printf("[%s] %s\n", time.Now().Format(time.UnixDate), msg) + return nil +}) + +// go about your business... +``` + +The SDK will emit messages during every telemetry submission. Successful +submissions will look something like this: + +``` +[Tue Nov 21 18:59:41 PST 2017] --------- Transmitting 16 items --------- +[Tue Nov 21 18:59:41 PST 2017] Telemetry transmitted in 708.382896ms +[Tue Nov 21 18:59:41 PST 2017] Response: 200 +``` + +If telemetry is rejected, the errors from the data collector endpoint will +be displayed: + +``` +[Tue Nov 21 18:58:39 PST 2017] --------- Transmitting 16 items --------- +[Tue Nov 21 18:58:40 PST 2017] Telemetry transmitted in 1.034608896s +[Tue Nov 21 18:58:40 PST 2017] Response: 206 +[Tue Nov 21 18:58:40 PST 2017] Items accepted/received: 15/16 +[Tue Nov 21 18:58:40 PST 2017] Errors: +[Tue Nov 21 18:58:40 PST 2017] #9 - 400 109: Field 'name' on type 'RemoteDependencyData' is required but missing or empty. Expected: string, Actual: +[Tue Nov 21 18:58:40 PST 2017] Telemetry item: + {"ver":1,"name":"Microsoft.ApplicationInsights.RemoteDependency","time":"2017-11-22T02:58:39Z","sampleRate":100,"seq":"","iKey":"","tags":{"ai.cloud.roleInstance":"","ai.device.id":"","ai.device.osVersion":"linux","ai.internal.sdkVersion":"go:0.4.0-pre","ai.operation.id":"bf755161-7725-490c-872e-69815826a94c"},"data":{"baseType":"RemoteDependencyData","baseData":{"ver":2,"name":"","id":"","resultCode":"","duration":"0.00:00:00.0000000","success":true,"data":"","target":"http://bing.com","type":"HTTP"}}} + +[Tue Nov 21 18:58:40 PST 2017] Refusing to retry telemetry submission (retry==false) +``` + +Information about retries, server throttling, and more from the SDK's +perspective will also be available. + +Please include this diagnostic information (with ikey's blocked out) when +submitting bug reports to this project. diff --git a/application-insights/SECURITY.md b/application-insights/SECURITY.md new file mode 100644 index 0000000000..869fdfe2b2 --- /dev/null +++ b/application-insights/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/application-insights/appinsights/client.go b/application-insights/appinsights/client.go new file mode 100644 index 0000000000..d532e03a00 --- /dev/null +++ b/application-insights/appinsights/client.go @@ -0,0 +1,155 @@ +package appinsights + +import ( + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +// Application Insights telemetry client provides interface to track telemetry +// items. +type TelemetryClient interface { + // Gets the telemetry context for this client. Values found on this + // context will get written out to every telemetry item tracked by + // this client. + Context() *TelemetryContext + + // Gets the instrumentation key assigned to this telemetry client. + InstrumentationKey() string + + // Gets the telemetry channel used to submit data to the backend. + Channel() TelemetryChannel + + // Gets whether this client is enabled and will accept telemetry. + IsEnabled() bool + + // Enables or disables the telemetry client. When disabled, telemetry + // is silently swallowed by the client. Defaults to enabled. + SetIsEnabled(enabled bool) + + // Submits the specified telemetry item. + Track(telemetry Telemetry) + + // Log a user action with the specified name + TrackEvent(name string) + + // Log a numeric value that is not specified with a specific event. + // Typically used to send regular reports of performance indicators. + TrackMetric(name string, value float64) + + // Log a trace message with the specified severity level. + TrackTrace(name string, severity contracts.SeverityLevel) + + // Log an HTTP request with the specified method, URL, duration and + // response code. + TrackRequest(method, url string, duration time.Duration, responseCode string) + + // Log a dependency with the specified name, type, target, and + // success status. + TrackRemoteDependency(name, dependencyType, target string, success bool) + + // Log an availability test result with the specified test name, + // duration, and success status. + TrackAvailability(name string, duration time.Duration, success bool) + + // Log an exception with the specified error, which may be a string, + // error or Stringer. The current callstack is collected + // automatically. + TrackException(err interface{}) +} + +type telemetryClient struct { + channel TelemetryChannel + context *TelemetryContext + isEnabled bool +} + +// Creates a new telemetry client instance that submits telemetry with the +// specified instrumentation key. +func NewTelemetryClient(iKey string) TelemetryClient { + return NewTelemetryClientFromConfig(NewTelemetryConfiguration(iKey)) +} + +// Creates a new telemetry client instance configured by the specified +// TelemetryConfiguration object. +func NewTelemetryClientFromConfig(config *TelemetryConfiguration) TelemetryClient { + return &telemetryClient{ + channel: NewInMemoryChannel(config), + context: config.setupContext(), + isEnabled: true, + } +} + +// Gets the telemetry context for this client. Values found on this context +// will get written out to every telemetry item tracked by this client. +func (tc *telemetryClient) Context() *TelemetryContext { + return tc.context +} + +// Gets the telemetry channel used to submit data to the backend. +func (tc *telemetryClient) Channel() TelemetryChannel { + return tc.channel +} + +// Gets the instrumentation key assigned to this telemetry client. +func (tc *telemetryClient) InstrumentationKey() string { + return tc.context.InstrumentationKey() +} + +// Gets whether this client is enabled and will accept telemetry. +func (tc *telemetryClient) IsEnabled() bool { + return tc.isEnabled +} + +// Enables or disables the telemetry client. When disabled, telemetry is +// silently swallowed by the client. Defaults to enabled. +func (tc *telemetryClient) SetIsEnabled(isEnabled bool) { + tc.isEnabled = isEnabled +} + +// Submits the specified telemetry item. +func (tc *telemetryClient) Track(item Telemetry) { + if tc.isEnabled && item != nil { + tc.channel.Send(tc.context.envelop(item)) + } +} + +// Log a user action with the specified name +func (tc *telemetryClient) TrackEvent(name string) { + tc.Track(NewEventTelemetry(name)) +} + +// Log a numeric value that is not specified with a specific event. +// Typically used to send regular reports of performance indicators. +func (tc *telemetryClient) TrackMetric(name string, value float64) { + tc.Track(NewMetricTelemetry(name, value)) +} + +// Log a trace message with the specified severity level. +func (tc *telemetryClient) TrackTrace(message string, severity contracts.SeverityLevel) { + tc.Track(NewTraceTelemetry(message, severity)) +} + +// Log an HTTP request with the specified method, URL, duration and response +// code. +func (tc *telemetryClient) TrackRequest(method, url string, duration time.Duration, responseCode string) { + tc.Track(NewRequestTelemetry(method, url, duration, responseCode)) +} + +// Log a dependency with the specified name, type, target, and success +// status. +func (tc *telemetryClient) TrackRemoteDependency(name, dependencyType, target string, success bool) { + tc.Track(NewRemoteDependencyTelemetry(name, dependencyType, target, success)) +} + +// Log an availability test result with the specified test name, duration, +// and success status. +func (tc *telemetryClient) TrackAvailability(name string, duration time.Duration, success bool) { + tc.Track(NewAvailabilityTelemetry(name, duration, success)) +} + +// Log an exception with the specified error, which may be a string, error +// or Stringer. The current callstack is collected automatically. +func (tc *telemetryClient) TrackException(err interface{}) { + tc.Track(newExceptionTelemetry(err, 1)) +} diff --git a/application-insights/appinsights/client_test.go b/application-insights/appinsights/client_test.go new file mode 100644 index 0000000000..740c43c339 --- /dev/null +++ b/application-insights/appinsights/client_test.go @@ -0,0 +1,236 @@ +package appinsights + +import ( + "bytes" + "compress/gzip" + "fmt" + "io/ioutil" + "strings" + "testing" + "time" +) + +func BenchmarkClientBurstPerformance(b *testing.B) { + client := NewTelemetryClient("") + client.(*telemetryClient).channel.(*InMemoryChannel).transmitter = &nullTransmitter{} + + for i := 0; i < b.N; i++ { + client.TrackTrace("A message", Information) + } + + <-client.Channel().Close(time.Minute) +} + +func TestClientProperties(t *testing.T) { + client := NewTelemetryClient(test_ikey) + defer client.Channel().Close() + + if _, ok := client.Channel().(*InMemoryChannel); !ok { + t.Error("Client's Channel() is not InMemoryChannel") + } + + if ikey := client.InstrumentationKey(); ikey != test_ikey { + t.Error("Client's InstrumentationKey is not expected") + } + + if ikey := client.Context().InstrumentationKey(); ikey != test_ikey { + t.Error("Context's InstrumentationKey is not expected") + } + + if client.Context() == nil { + t.Error("Client.Context == nil") + } + + if client.IsEnabled() == false { + t.Error("Client.IsEnabled == false") + } + + client.SetIsEnabled(false) + if client.IsEnabled() == true { + t.Error("Client.SetIsEnabled had no effect") + } + + if client.Channel().EndpointAddress() != "https://dc.services.visualstudio.com/v2/track" { + t.Error("Client.Channel.EndpointAddress was incorrect") + } +} + +func TestClientPropertiesWithConnectionString(t *testing.T) { + client := NewTelemetryClientFromConfig(NewTelemetryConfigurationWithConnectionString(connection_string)) + defer client.Channel().Close() + + if _, ok := client.Channel().(*InMemoryChannel); !ok { + t.Error("Client's Channel() is not InMemoryChannel") + } + + if ikey := client.InstrumentationKey(); ikey != "00000000-0000-0000-0000-000000000000" { + t.Error("Client's InstrumentationKey is not expected") + } + + if ikey := client.Context().InstrumentationKey(); ikey != "00000000-0000-0000-0000-000000000000" { + t.Error("Context's InstrumentationKey is not expected") + } + + if client.Context() == nil { + t.Error("Client.Context == nil") + } + + if client.IsEnabled() == false { + t.Error("Client.IsEnabled == false") + } + + client.SetIsEnabled(false) + if client.IsEnabled() == true { + t.Error("Client.SetIsEnabled had no effect") + } + + if client.Channel().EndpointAddress() != "https://ingestion.endpoint.com/v2/track" { + t.Error("Client.Channel.EndpointAddress was incorrect") + } +} + +func TestEndToEnd(t *testing.T) { + mockClock(time.Unix(1511001321, 0)) + defer resetClock() + xmit, server := newTestClientServer() + defer server.Close() + + config := NewTelemetryConfiguration(test_ikey) + config.EndpointUrl = xmit.(*httpTransmitter).endpoint + client := NewTelemetryClientFromConfig(config) + defer client.Channel().Close() + + // Track directly off the client + client.TrackEvent("client-event") + client.TrackMetric("client-metric", 44.0) + client.TrackTrace("client-trace", Information) + client.TrackRequest("GET", "www.testurl.org", time.Minute, "404") + + // NOTE: A lot of this is covered elsewhere, so we won't duplicate + // *too* much. + + // Set up server response + server.responseData = []byte(`{"itemsReceived":4, "itemsAccepted":4, "errors":[]}`) + server.responseHeaders["Content-type"] = "application/json" + + // Wait for automatic transmit -- get the request + slowTick(11) + req := server.waitForRequest(t) + + // GZIP magic number + if len(req.body) < 2 || req.body[0] != 0x1f || req.body[1] != 0x8b { + t.Fatal("Missing gzip magic number") + } + + // Decompress + reader, err := gzip.NewReader(bytes.NewReader(req.body)) + if err != nil { + t.Fatalf("Coudln't create gzip reader: %s", err.Error()) + } + + // Read payload + body, err := ioutil.ReadAll(reader) + reader.Close() + if err != nil { + t.Fatalf("Couldn't read compressed data: %s", err.Error()) + } + + // Check out payload + j, err := parsePayload(body) + if err != nil { + t.Errorf("Error parsing payload: %s", err.Error()) + } + + if len(j) != 4 { + t.Fatal("Unexpected event count") + } + + j[0].assertPath(t, "iKey", test_ikey) + j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") + j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") + + j[1].assertPath(t, "iKey", test_ikey) + j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") + j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") + + j[2].assertPath(t, "iKey", test_ikey) + j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") + j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") + + j[3].assertPath(t, "iKey", test_ikey) + j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") + j[3].assertPath(t, "time", "2017-11-18T10:34:21Z") +} + +func TestEndToEndWithConnectionString(t *testing.T) { + mockClock(time.Unix(1511001321, 0)) + defer resetClock() + xmit, server := newTestClientServer() + defer server.Close() + + baseEndpoint := strings.TrimSuffix(xmit.(*httpTransmitter).endpoint, "v2/track") + testConnectionString := fmt.Sprintf("InstrumentationKey=%s;IngestionEndpoint=%s", test_ikey, baseEndpoint) + client := NewTelemetryClientFromConfig(NewTelemetryConfigurationWithConnectionString(testConnectionString)) + defer client.Channel().Close() + + // Track directly off the client + client.TrackEvent("client-event") + client.TrackMetric("client-metric", 44.0) + client.TrackTrace("client-trace", Information) + client.TrackRequest("GET", "www.testurl.org", time.Minute, "404") + + // NOTE: A lot of this is covered elsewhere, so we won't duplicate + // *too* much. + + // Set up server response + server.responseData = []byte(`{"itemsReceived":4, "itemsAccepted":4, "errors":[]}`) + server.responseHeaders["Content-type"] = "application/json" + + // Wait for automatic transmit -- get the request + slowTick(11) + req := server.waitForRequest(t) + + // GZIP magic number + if len(req.body) < 2 || req.body[0] != 0x1f || req.body[1] != 0x8b { + t.Fatal("Missing gzip magic number") + } + + // Decompress + reader, err := gzip.NewReader(bytes.NewReader(req.body)) + if err != nil { + t.Fatalf("Coudln't create gzip reader: %s", err.Error()) + } + + // Read payload + body, err := ioutil.ReadAll(reader) + reader.Close() + if err != nil { + t.Fatalf("Couldn't read compressed data: %s", err.Error()) + } + + // Check out payload + j, err := parsePayload(body) + if err != nil { + t.Errorf("Error parsing payload: %s", err.Error()) + } + + if len(j) != 4 { + t.Fatal("Unexpected event count") + } + + j[0].assertPath(t, "iKey", test_ikey) + j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") + j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") + + j[1].assertPath(t, "iKey", test_ikey) + j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") + j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") + + j[2].assertPath(t, "iKey", test_ikey) + j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") + j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") + + j[3].assertPath(t, "iKey", test_ikey) + j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") + j[3].assertPath(t, "time", "2017-11-18T10:34:21Z") +} diff --git a/application-insights/appinsights/clock.go b/application-insights/appinsights/clock.go new file mode 100644 index 0000000000..1178b9eaa7 --- /dev/null +++ b/application-insights/appinsights/clock.go @@ -0,0 +1,11 @@ +package appinsights + +// We need to mock out the clock for tests; we'll use this to do it. + +import "code.cloudfoundry.org/clock" + +var currentClock clock.Clock + +func init() { + currentClock = clock.NewClock() +} diff --git a/application-insights/appinsights/clock_test.go b/application-insights/appinsights/clock_test.go new file mode 100644 index 0000000000..8da85b6f9a --- /dev/null +++ b/application-insights/appinsights/clock_test.go @@ -0,0 +1,39 @@ +package appinsights + +import ( + "time" + + "code.cloudfoundry.org/clock" + "code.cloudfoundry.org/clock/fakeclock" +) + +var fakeClock *fakeclock.FakeClock + +func mockClock(timestamp ...time.Time) { + if len(timestamp) > 0 { + fakeClock = fakeclock.NewFakeClock(timestamp[0]) + } else { + fakeClock = fakeclock.NewFakeClock(time.Now().Round(time.Minute)) + } + + currentClock = fakeClock +} + +func resetClock() { + fakeClock = nil + currentClock = clock.NewClock() +} + +func slowTick(seconds int) { + const delay = time.Millisecond * time.Duration(5) + + // Sleeps in tests are evil, but with all the async nonsense going + // on, no callbacks, and minimal control of the clock, I'm not + // really sure I have another choice. + + time.Sleep(delay) + for i := 0; i < seconds; i++ { + fakeClock.Increment(time.Second) + time.Sleep(delay) + } +} diff --git a/application-insights/appinsights/configuration.go b/application-insights/appinsights/configuration.go new file mode 100644 index 0000000000..c21fa9f1f1 --- /dev/null +++ b/application-insights/appinsights/configuration.go @@ -0,0 +1,68 @@ +package appinsights + +import ( + "net/http" + "os" + "runtime" + "time" +) + +// Configuration data used to initialize a new TelemetryClient. +type TelemetryConfiguration struct { + // Instrumentation key for the client. + InstrumentationKey string + + // Endpoint URL where data will be submitted. + EndpointUrl string + + // Maximum number of telemetry items that can be submitted in each + // request. If this many items are buffered, the buffer will be + // flushed before MaxBatchInterval expires. + MaxBatchSize int + + // Maximum time to wait before sending a batch of telemetry. + MaxBatchInterval time.Duration + + // Customized http client if desired (will use http.DefaultClient otherwise) + Client *http.Client +} + +// Creates a new TelemetryConfiguration object with the specified +// instrumentation key and default values. +func NewTelemetryConfiguration(instrumentationKey string) *TelemetryConfiguration { + return &TelemetryConfiguration{ + InstrumentationKey: instrumentationKey, + EndpointUrl: "https://dc.services.visualstudio.com/v2/track", + MaxBatchSize: 1024, + MaxBatchInterval: time.Duration(10) * time.Second, + } +} + +// Creates a new TelemetryConfiguration object with the specified +// connection string and endpoint URL. +func NewTelemetryConfigurationWithConnectionString(connectionString string) *TelemetryConfiguration { + connectionParams, err := parseConnectionString(connectionString) + if err != nil { + return nil + } + + return &TelemetryConfiguration{ + InstrumentationKey: connectionParams.InstrumentationKey, + EndpointUrl: connectionParams.IngestionEndpoint, + MaxBatchSize: 1024, + MaxBatchInterval: time.Duration(10) * time.Second, + } +} + +func (config *TelemetryConfiguration) setupContext() *TelemetryContext { + context := NewTelemetryContext(config.InstrumentationKey) + context.Tags.Internal().SetSdkVersion(sdkName + ":" + Version) + context.Tags.Device().SetOsVersion(runtime.GOOS) + + if hostname, err := os.Hostname(); err == nil { + context.Tags.Device().SetId(hostname) + context.Tags.Cloud().SetRoleInstance(hostname) + } + + return context +} diff --git a/application-insights/appinsights/configuration_test.go b/application-insights/appinsights/configuration_test.go new file mode 100644 index 0000000000..8404092a72 --- /dev/null +++ b/application-insights/appinsights/configuration_test.go @@ -0,0 +1,38 @@ +package appinsights + +import "testing" + +func TestTelemetryConfiguration(t *testing.T) { + testKey := "test" + defaultEndpoint := "https://dc.services.visualstudio.com/v2/track" + + config := NewTelemetryConfiguration(testKey) + + if config.InstrumentationKey != testKey { + t.Errorf("InstrumentationKey is %s, want %s", config.InstrumentationKey, testKey) + } + + if config.EndpointUrl != defaultEndpoint { + t.Errorf("EndpointUrl is %s, want %s", config.EndpointUrl, defaultEndpoint) + } + + if config.Client != nil { + t.Errorf("Client is not nil, want nil") + } +} + +func TestTelemetryConfigurationWithConnectionString(t *testing.T) { + config := NewTelemetryConfigurationWithConnectionString(connection_string) + + if config.InstrumentationKey != "00000000-0000-0000-0000-000000000000" { + t.Errorf("InstrumentationKey is %s, want 00000000-0000-0000-0000-000000000000", config.InstrumentationKey) + } + + if config.EndpointUrl != "https://ingestion.endpoint.com/v2/track" { + t.Errorf("EndpointUrl is %s, want https://ingestion.endpoint.com/v2/track", config.EndpointUrl) + } + + if config.Client != nil { + t.Errorf("Client is not nil, want nil") + } +} diff --git a/application-insights/appinsights/connection_string_parser.go b/application-insights/appinsights/connection_string_parser.go new file mode 100644 index 0000000000..04e963ec2b --- /dev/null +++ b/application-insights/appinsights/connection_string_parser.go @@ -0,0 +1,39 @@ +package appinsights + +import ( + "errors" + "strings" +) + +type ConnectionParams struct { + InstrumentationKey string + IngestionEndpoint string +} + +func parseConnectionString(cString string) (*ConnectionParams, error) { + connectionParams := &ConnectionParams{} + + pairs := strings.Split(cString, ";") + for _, pair := range pairs { + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + return nil, errors.New("invalid connection parameter format") + } + + key, value := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]) + + switch strings.ToLower(key) { + case "instrumentationkey": + connectionParams.InstrumentationKey = value + case "ingestionendpoint": + if value != "" { + connectionParams.IngestionEndpoint = value + "v2/track" + } + } + } + + if connectionParams.InstrumentationKey == "" || connectionParams.IngestionEndpoint == "" { + return nil, errors.New("missing required connection parameters") + } + return connectionParams, nil +} diff --git a/application-insights/appinsights/connection_string_parser_test.go b/application-insights/appinsights/connection_string_parser_test.go new file mode 100644 index 0000000000..87cb718f25 --- /dev/null +++ b/application-insights/appinsights/connection_string_parser_test.go @@ -0,0 +1,66 @@ +package appinsights + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const connection_string = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" + +func TestConnectionStringParser(t *testing.T) { + tests := []struct { + name string + cString string + want *ConnectionParams + wantErr bool + }{ + { + name: "Valid connection string and instrumentation key", + cString: connection_string, + want: &ConnectionParams{ + InstrumentationKey: "00000000-0000-0000-0000-000000000000", + IngestionEndpoint: "https://ingestion.endpoint.com/v2/track", + }, + wantErr: false, + }, + { + name: "Invalid connection string format", + cString: "Invalid connection string", + want: nil, + wantErr: true, + }, + { + name: "Valid instrumentation key with missing ingestion endpoint", + cString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=", + want: nil, + wantErr: true, + }, + { + name: "Missing instrumentation key with valid ingestion endpoint", + cString: "InstrumentationKey=;IngestionEndpoint=https://ingestion.endpoint.com/v2/track", + want: nil, + wantErr: true, + }, + { + name: "Empty connection string", + cString: "", + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseConnectionString(tt.cString) + if tt.wantErr { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Expected no error but got one") + require.NotNil(t, got, "Expected a non-nil result") + require.Equal(t, tt.want.InstrumentationKey, got.InstrumentationKey, "Instrumentation Key does not match") + require.Equal(t, tt.want.IngestionEndpoint, got.IngestionEndpoint, "Ingestion Endpoint does not match") + } + }) + } +} diff --git a/application-insights/appinsights/constants.go b/application-insights/appinsights/constants.go new file mode 100644 index 0000000000..060ed59d4e --- /dev/null +++ b/application-insights/appinsights/constants.go @@ -0,0 +1,20 @@ +package appinsights + +// NOTE: This file was automatically generated. + +import "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" + +// Type of the metric data measurement. +const ( + Measurement contracts.DataPointType = contracts.Measurement + Aggregation contracts.DataPointType = contracts.Aggregation +) + +// Defines the level of severity for the event. +const ( + Verbose contracts.SeverityLevel = contracts.Verbose + Information contracts.SeverityLevel = contracts.Information + Warning contracts.SeverityLevel = contracts.Warning + Error contracts.SeverityLevel = contracts.Error + Critical contracts.SeverityLevel = contracts.Critical +) diff --git a/application-insights/appinsights/contracts/availabilitydata.go b/application-insights/appinsights/contracts/availabilitydata.go new file mode 100644 index 0000000000..4f0d709f5c --- /dev/null +++ b/application-insights/appinsights/contracts/availabilitydata.go @@ -0,0 +1,111 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Instances of AvailabilityData represent the result of executing an +// availability test. +type AvailabilityData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // Identifier of a test run. Use it to correlate steps of test run and + // telemetry generated by the service. + Id string `json:"id"` + + // Name of the test that these availability results represent. + Name string `json:"name"` + + // Duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 days. + Duration string `json:"duration"` + + // Success flag. + Success bool `json:"success"` + + // Name of the location where the test was run from. + RunLocation string `json:"runLocation"` + + // Diagnostic message for the result. + Message string `json:"message"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` + + // Collection of custom measurements. + Measurements map[string]float64 `json:"measurements,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *AvailabilityData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".Availability" + } else { + return "Microsoft.ApplicationInsights.Availability" + } +} + +// Returns the base type when placed within a Data object container. +func (data *AvailabilityData) BaseType() string { + return "AvailabilityData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *AvailabilityData) Sanitize() []string { + var warnings []string + + if len(data.Id) > 64 { + data.Id = data.Id[:64] + warnings = append(warnings, "AvailabilityData.Id exceeded maximum length of 64") + } + + if len(data.Name) > 1024 { + data.Name = data.Name[:1024] + warnings = append(warnings, "AvailabilityData.Name exceeded maximum length of 1024") + } + + if len(data.RunLocation) > 1024 { + data.RunLocation = data.RunLocation[:1024] + warnings = append(warnings, "AvailabilityData.RunLocation exceeded maximum length of 1024") + } + + if len(data.Message) > 8192 { + data.Message = data.Message[:8192] + warnings = append(warnings, "AvailabilityData.Message exceeded maximum length of 8192") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "AvailabilityData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "AvailabilityData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + if data.Measurements != nil { + for k, v := range data.Measurements { + if len(k) > 150 { + data.Measurements[k[:150]] = v + delete(data.Measurements, k) + warnings = append(warnings, "AvailabilityData.Measurements has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new AvailabilityData instance with default values set by the schema. +func NewAvailabilityData() *AvailabilityData { + return &AvailabilityData{ + Ver: 2, + } +} diff --git a/application-insights/appinsights/contracts/base.go b/application-insights/appinsights/contracts/base.go new file mode 100644 index 0000000000..3ceb5022f2 --- /dev/null +++ b/application-insights/appinsights/contracts/base.go @@ -0,0 +1,25 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Data struct to contain only C section with custom fields. +type Base struct { + + // Name of item (B section) if any. If telemetry data is derived straight from + // this, this should be null. + BaseType string `json:"baseType"` +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *Base) Sanitize() []string { + var warnings []string + + return warnings +} + +// Creates a new Base instance with default values set by the schema. +func NewBase() *Base { + return &Base{} +} diff --git a/application-insights/appinsights/contracts/contexttagkeys.go b/application-insights/appinsights/contracts/contexttagkeys.go new file mode 100644 index 0000000000..eaf57abb31 --- /dev/null +++ b/application-insights/appinsights/contracts/contexttagkeys.go @@ -0,0 +1,153 @@ +package contracts + +// NOTE: This file was automatically generated. + +import "strconv" + +const ( + // Application version. Information in the application context fields is + // always about the application that is sending the telemetry. + ApplicationVersion string = "ai.application.ver" + + // Unique client device id. Computer name in most cases. + DeviceId string = "ai.device.id" + + // Device locale using - pattern, following RFC 5646. + // Example 'en-US'. + DeviceLocale string = "ai.device.locale" + + // Model of the device the end user of the application is using. Used for + // client scenarios. If this field is empty then it is derived from the user + // agent. + DeviceModel string = "ai.device.model" + + // Client device OEM name taken from the browser. + DeviceOEMName string = "ai.device.oemName" + + // Operating system name and version of the device the end user of the + // application is using. If this field is empty then it is derived from the + // user agent. Example 'Windows 10 Pro 10.0.10586.0' + DeviceOSVersion string = "ai.device.osVersion" + + // The type of the device the end user of the application is using. Used + // primarily to distinguish JavaScript telemetry from server side telemetry. + // Examples: 'PC', 'Phone', 'Browser'. 'PC' is the default value. + DeviceType string = "ai.device.type" + + // The IP address of the client device. IPv4 and IPv6 are supported. + // Information in the location context fields is always about the end user. + // When telemetry is sent from a service, the location context is about the + // user that initiated the operation in the service. + LocationIp string = "ai.location.ip" + + // A unique identifier for the operation instance. The operation.id is created + // by either a request or a page view. All other telemetry sets this to the + // value for the containing request or page view. Operation.id is used for + // finding all the telemetry items for a specific operation instance. + OperationId string = "ai.operation.id" + + // The name (group) of the operation. The operation.name is created by either + // a request or a page view. All other telemetry items set this to the value + // for the containing request or page view. Operation.name is used for finding + // all the telemetry items for a group of operations (i.e. 'GET Home/Index'). + OperationName string = "ai.operation.name" + + // The unique identifier of the telemetry item's immediate parent. + OperationParentId string = "ai.operation.parentId" + + // Name of synthetic source. Some telemetry from the application may represent + // a synthetic traffic. It may be web crawler indexing the web site, site + // availability tests or traces from diagnostic libraries like Application + // Insights SDK itself. + OperationSyntheticSource string = "ai.operation.syntheticSource" + + // The correlation vector is a light weight vector clock which can be used to + // identify and order related events across clients and services. + OperationCorrelationVector string = "ai.operation.correlationVector" + + // Session ID - the instance of the user's interaction with the app. + // Information in the session context fields is always about the end user. + // When telemetry is sent from a service, the session context is about the + // user that initiated the operation in the service. + SessionId string = "ai.session.id" + + // Boolean value indicating whether the session identified by ai.session.id is + // first for the user or not. + SessionIsFirst string = "ai.session.isFirst" + + // In multi-tenant applications this is the account ID or name which the user + // is acting with. Examples may be subscription ID for Azure portal or blog + // name blogging platform. + UserAccountId string = "ai.user.accountId" + + // Anonymous user id. Represents the end user of the application. When + // telemetry is sent from a service, the user context is about the user that + // initiated the operation in the service. + UserId string = "ai.user.id" + + // Authenticated user id. The opposite of ai.user.id, this represents the user + // with a friendly name. Since it's PII information it is not collected by + // default by most SDKs. + UserAuthUserId string = "ai.user.authUserId" + + // Name of the role the application is a part of. Maps directly to the role + // name in azure. + CloudRole string = "ai.cloud.role" + + // Name of the instance where the application is running. Computer name for + // on-premisis, instance name for Azure. + CloudRoleInstance string = "ai.cloud.roleInstance" + + // SDK version. See + // https://github.com/microsoft/ApplicationInsights-Home/blob/master/SDK-AUTHORING.md#sdk-version-specification + // for information. + InternalSdkVersion string = "ai.internal.sdkVersion" + + // Agent version. Used to indicate the version of StatusMonitor installed on + // the computer if it is used for data collection. + InternalAgentVersion string = "ai.internal.agentVersion" + + // This is the node name used for billing purposes. Use it to override the + // standard detection of nodes. + InternalNodeName string = "ai.internal.nodeName" +) + +var tagMaxLengths = map[string]int{ + "ai.application.ver": 1024, + "ai.device.id": 1024, + "ai.device.locale": 64, + "ai.device.model": 256, + "ai.device.oemName": 256, + "ai.device.osVersion": 256, + "ai.device.type": 64, + "ai.location.ip": 46, + "ai.operation.id": 128, + "ai.operation.name": 1024, + "ai.operation.parentId": 128, + "ai.operation.syntheticSource": 1024, + "ai.operation.correlationVector": 64, + "ai.session.id": 64, + "ai.session.isFirst": 5, + "ai.user.accountId": 1024, + "ai.user.id": 128, + "ai.user.authUserId": 1024, + "ai.cloud.role": 256, + "ai.cloud.roleInstance": 256, + "ai.internal.sdkVersion": 64, + "ai.internal.agentVersion": 64, + "ai.internal.nodeName": 256, +} + +// Truncates tag values that exceed their maximum supported lengths. Returns +// warnings for each affected field. +func SanitizeTags(tags map[string]string) []string { + var warnings []string + for k, v := range tags { + if maxlen, ok := tagMaxLengths[k]; ok && len(v) > maxlen { + tags[k] = v[:maxlen] + warnings = append(warnings, "Value for "+k+" exceeded maximum length of "+strconv.Itoa(maxlen)) + } + } + + return warnings +} diff --git a/application-insights/appinsights/contracts/contexttags.go b/application-insights/appinsights/contracts/contexttags.go new file mode 100644 index 0000000000..426378318b --- /dev/null +++ b/application-insights/appinsights/contracts/contexttags.go @@ -0,0 +1,565 @@ +package contracts + +// NOTE: This file was automatically generated. + +type ContextTags map[string]string + +// Helper type that provides access to context fields grouped under 'application'. +// This is returned by TelemetryContext.Tags.Application() +type ApplicationContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'device'. +// This is returned by TelemetryContext.Tags.Device() +type DeviceContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'location'. +// This is returned by TelemetryContext.Tags.Location() +type LocationContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'operation'. +// This is returned by TelemetryContext.Tags.Operation() +type OperationContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'session'. +// This is returned by TelemetryContext.Tags.Session() +type SessionContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'user'. +// This is returned by TelemetryContext.Tags.User() +type UserContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'cloud'. +// This is returned by TelemetryContext.Tags.Cloud() +type CloudContextTags ContextTags + +// Helper type that provides access to context fields grouped under 'internal'. +// This is returned by TelemetryContext.Tags.Internal() +type InternalContextTags ContextTags + +// Returns a helper to access context fields grouped under 'application'. +func (tags ContextTags) Application() ApplicationContextTags { + return ApplicationContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'device'. +func (tags ContextTags) Device() DeviceContextTags { + return DeviceContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'location'. +func (tags ContextTags) Location() LocationContextTags { + return LocationContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'operation'. +func (tags ContextTags) Operation() OperationContextTags { + return OperationContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'session'. +func (tags ContextTags) Session() SessionContextTags { + return SessionContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'user'. +func (tags ContextTags) User() UserContextTags { + return UserContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'cloud'. +func (tags ContextTags) Cloud() CloudContextTags { + return CloudContextTags(tags) +} + +// Returns a helper to access context fields grouped under 'internal'. +func (tags ContextTags) Internal() InternalContextTags { + return InternalContextTags(tags) +} + +// Application version. Information in the application context fields is +// always about the application that is sending the telemetry. +func (tags ApplicationContextTags) GetVer() string { + if result, ok := tags["ai.application.ver"]; ok { + return result + } + + return "" +} + +// Application version. Information in the application context fields is +// always about the application that is sending the telemetry. +func (tags ApplicationContextTags) SetVer(value string) { + if value != "" { + tags["ai.application.ver"] = value + } else { + delete(tags, "ai.application.ver") + } +} + +// Unique client device id. Computer name in most cases. +func (tags DeviceContextTags) GetId() string { + if result, ok := tags["ai.device.id"]; ok { + return result + } + + return "" +} + +// Unique client device id. Computer name in most cases. +func (tags DeviceContextTags) SetId(value string) { + if value != "" { + tags["ai.device.id"] = value + } else { + delete(tags, "ai.device.id") + } +} + +// Device locale using - pattern, following RFC 5646. +// Example 'en-US'. +func (tags DeviceContextTags) GetLocale() string { + if result, ok := tags["ai.device.locale"]; ok { + return result + } + + return "" +} + +// Device locale using - pattern, following RFC 5646. +// Example 'en-US'. +func (tags DeviceContextTags) SetLocale(value string) { + if value != "" { + tags["ai.device.locale"] = value + } else { + delete(tags, "ai.device.locale") + } +} + +// Model of the device the end user of the application is using. Used for +// client scenarios. If this field is empty then it is derived from the user +// agent. +func (tags DeviceContextTags) GetModel() string { + if result, ok := tags["ai.device.model"]; ok { + return result + } + + return "" +} + +// Model of the device the end user of the application is using. Used for +// client scenarios. If this field is empty then it is derived from the user +// agent. +func (tags DeviceContextTags) SetModel(value string) { + if value != "" { + tags["ai.device.model"] = value + } else { + delete(tags, "ai.device.model") + } +} + +// Client device OEM name taken from the browser. +func (tags DeviceContextTags) GetOemName() string { + if result, ok := tags["ai.device.oemName"]; ok { + return result + } + + return "" +} + +// Client device OEM name taken from the browser. +func (tags DeviceContextTags) SetOemName(value string) { + if value != "" { + tags["ai.device.oemName"] = value + } else { + delete(tags, "ai.device.oemName") + } +} + +// Operating system name and version of the device the end user of the +// application is using. If this field is empty then it is derived from the +// user agent. Example 'Windows 10 Pro 10.0.10586.0' +func (tags DeviceContextTags) GetOsVersion() string { + if result, ok := tags["ai.device.osVersion"]; ok { + return result + } + + return "" +} + +// Operating system name and version of the device the end user of the +// application is using. If this field is empty then it is derived from the +// user agent. Example 'Windows 10 Pro 10.0.10586.0' +func (tags DeviceContextTags) SetOsVersion(value string) { + if value != "" { + tags["ai.device.osVersion"] = value + } else { + delete(tags, "ai.device.osVersion") + } +} + +// The type of the device the end user of the application is using. Used +// primarily to distinguish JavaScript telemetry from server side telemetry. +// Examples: 'PC', 'Phone', 'Browser'. 'PC' is the default value. +func (tags DeviceContextTags) GetType() string { + if result, ok := tags["ai.device.type"]; ok { + return result + } + + return "" +} + +// The type of the device the end user of the application is using. Used +// primarily to distinguish JavaScript telemetry from server side telemetry. +// Examples: 'PC', 'Phone', 'Browser'. 'PC' is the default value. +func (tags DeviceContextTags) SetType(value string) { + if value != "" { + tags["ai.device.type"] = value + } else { + delete(tags, "ai.device.type") + } +} + +// The IP address of the client device. IPv4 and IPv6 are supported. +// Information in the location context fields is always about the end user. +// When telemetry is sent from a service, the location context is about the +// user that initiated the operation in the service. +func (tags LocationContextTags) GetIp() string { + if result, ok := tags["ai.location.ip"]; ok { + return result + } + + return "" +} + +// The IP address of the client device. IPv4 and IPv6 are supported. +// Information in the location context fields is always about the end user. +// When telemetry is sent from a service, the location context is about the +// user that initiated the operation in the service. +func (tags LocationContextTags) SetIp(value string) { + if value != "" { + tags["ai.location.ip"] = value + } else { + delete(tags, "ai.location.ip") + } +} + +// A unique identifier for the operation instance. The operation.id is created +// by either a request or a page view. All other telemetry sets this to the +// value for the containing request or page view. Operation.id is used for +// finding all the telemetry items for a specific operation instance. +func (tags OperationContextTags) GetId() string { + if result, ok := tags["ai.operation.id"]; ok { + return result + } + + return "" +} + +// A unique identifier for the operation instance. The operation.id is created +// by either a request or a page view. All other telemetry sets this to the +// value for the containing request or page view. Operation.id is used for +// finding all the telemetry items for a specific operation instance. +func (tags OperationContextTags) SetId(value string) { + if value != "" { + tags["ai.operation.id"] = value + } else { + delete(tags, "ai.operation.id") + } +} + +// The name (group) of the operation. The operation.name is created by either +// a request or a page view. All other telemetry items set this to the value +// for the containing request or page view. Operation.name is used for finding +// all the telemetry items for a group of operations (i.e. 'GET Home/Index'). +func (tags OperationContextTags) GetName() string { + if result, ok := tags["ai.operation.name"]; ok { + return result + } + + return "" +} + +// The name (group) of the operation. The operation.name is created by either +// a request or a page view. All other telemetry items set this to the value +// for the containing request or page view. Operation.name is used for finding +// all the telemetry items for a group of operations (i.e. 'GET Home/Index'). +func (tags OperationContextTags) SetName(value string) { + if value != "" { + tags["ai.operation.name"] = value + } else { + delete(tags, "ai.operation.name") + } +} + +// The unique identifier of the telemetry item's immediate parent. +func (tags OperationContextTags) GetParentId() string { + if result, ok := tags["ai.operation.parentId"]; ok { + return result + } + + return "" +} + +// The unique identifier of the telemetry item's immediate parent. +func (tags OperationContextTags) SetParentId(value string) { + if value != "" { + tags["ai.operation.parentId"] = value + } else { + delete(tags, "ai.operation.parentId") + } +} + +// Name of synthetic source. Some telemetry from the application may represent +// a synthetic traffic. It may be web crawler indexing the web site, site +// availability tests or traces from diagnostic libraries like Application +// Insights SDK itself. +func (tags OperationContextTags) GetSyntheticSource() string { + if result, ok := tags["ai.operation.syntheticSource"]; ok { + return result + } + + return "" +} + +// Name of synthetic source. Some telemetry from the application may represent +// a synthetic traffic. It may be web crawler indexing the web site, site +// availability tests or traces from diagnostic libraries like Application +// Insights SDK itself. +func (tags OperationContextTags) SetSyntheticSource(value string) { + if value != "" { + tags["ai.operation.syntheticSource"] = value + } else { + delete(tags, "ai.operation.syntheticSource") + } +} + +// The correlation vector is a light weight vector clock which can be used to +// identify and order related events across clients and services. +func (tags OperationContextTags) GetCorrelationVector() string { + if result, ok := tags["ai.operation.correlationVector"]; ok { + return result + } + + return "" +} + +// The correlation vector is a light weight vector clock which can be used to +// identify and order related events across clients and services. +func (tags OperationContextTags) SetCorrelationVector(value string) { + if value != "" { + tags["ai.operation.correlationVector"] = value + } else { + delete(tags, "ai.operation.correlationVector") + } +} + +// Session ID - the instance of the user's interaction with the app. +// Information in the session context fields is always about the end user. +// When telemetry is sent from a service, the session context is about the +// user that initiated the operation in the service. +func (tags SessionContextTags) GetId() string { + if result, ok := tags["ai.session.id"]; ok { + return result + } + + return "" +} + +// Session ID - the instance of the user's interaction with the app. +// Information in the session context fields is always about the end user. +// When telemetry is sent from a service, the session context is about the +// user that initiated the operation in the service. +func (tags SessionContextTags) SetId(value string) { + if value != "" { + tags["ai.session.id"] = value + } else { + delete(tags, "ai.session.id") + } +} + +// Boolean value indicating whether the session identified by ai.session.id is +// first for the user or not. +func (tags SessionContextTags) GetIsFirst() string { + if result, ok := tags["ai.session.isFirst"]; ok { + return result + } + + return "" +} + +// Boolean value indicating whether the session identified by ai.session.id is +// first for the user or not. +func (tags SessionContextTags) SetIsFirst(value string) { + if value != "" { + tags["ai.session.isFirst"] = value + } else { + delete(tags, "ai.session.isFirst") + } +} + +// In multi-tenant applications this is the account ID or name which the user +// is acting with. Examples may be subscription ID for Azure portal or blog +// name blogging platform. +func (tags UserContextTags) GetAccountId() string { + if result, ok := tags["ai.user.accountId"]; ok { + return result + } + + return "" +} + +// In multi-tenant applications this is the account ID or name which the user +// is acting with. Examples may be subscription ID for Azure portal or blog +// name blogging platform. +func (tags UserContextTags) SetAccountId(value string) { + if value != "" { + tags["ai.user.accountId"] = value + } else { + delete(tags, "ai.user.accountId") + } +} + +// Anonymous user id. Represents the end user of the application. When +// telemetry is sent from a service, the user context is about the user that +// initiated the operation in the service. +func (tags UserContextTags) GetId() string { + if result, ok := tags["ai.user.id"]; ok { + return result + } + + return "" +} + +// Anonymous user id. Represents the end user of the application. When +// telemetry is sent from a service, the user context is about the user that +// initiated the operation in the service. +func (tags UserContextTags) SetId(value string) { + if value != "" { + tags["ai.user.id"] = value + } else { + delete(tags, "ai.user.id") + } +} + +// Authenticated user id. The opposite of ai.user.id, this represents the user +// with a friendly name. Since it's PII information it is not collected by +// default by most SDKs. +func (tags UserContextTags) GetAuthUserId() string { + if result, ok := tags["ai.user.authUserId"]; ok { + return result + } + + return "" +} + +// Authenticated user id. The opposite of ai.user.id, this represents the user +// with a friendly name. Since it's PII information it is not collected by +// default by most SDKs. +func (tags UserContextTags) SetAuthUserId(value string) { + if value != "" { + tags["ai.user.authUserId"] = value + } else { + delete(tags, "ai.user.authUserId") + } +} + +// Name of the role the application is a part of. Maps directly to the role +// name in azure. +func (tags CloudContextTags) GetRole() string { + if result, ok := tags["ai.cloud.role"]; ok { + return result + } + + return "" +} + +// Name of the role the application is a part of. Maps directly to the role +// name in azure. +func (tags CloudContextTags) SetRole(value string) { + if value != "" { + tags["ai.cloud.role"] = value + } else { + delete(tags, "ai.cloud.role") + } +} + +// Name of the instance where the application is running. Computer name for +// on-premisis, instance name for Azure. +func (tags CloudContextTags) GetRoleInstance() string { + if result, ok := tags["ai.cloud.roleInstance"]; ok { + return result + } + + return "" +} + +// Name of the instance where the application is running. Computer name for +// on-premisis, instance name for Azure. +func (tags CloudContextTags) SetRoleInstance(value string) { + if value != "" { + tags["ai.cloud.roleInstance"] = value + } else { + delete(tags, "ai.cloud.roleInstance") + } +} + +// SDK version. See +// https://github.com/microsoft/ApplicationInsights-Home/blob/master/SDK-AUTHORING.md#sdk-version-specification +// for information. +func (tags InternalContextTags) GetSdkVersion() string { + if result, ok := tags["ai.internal.sdkVersion"]; ok { + return result + } + + return "" +} + +// SDK version. See +// https://github.com/microsoft/ApplicationInsights-Home/blob/master/SDK-AUTHORING.md#sdk-version-specification +// for information. +func (tags InternalContextTags) SetSdkVersion(value string) { + if value != "" { + tags["ai.internal.sdkVersion"] = value + } else { + delete(tags, "ai.internal.sdkVersion") + } +} + +// Agent version. Used to indicate the version of StatusMonitor installed on +// the computer if it is used for data collection. +func (tags InternalContextTags) GetAgentVersion() string { + if result, ok := tags["ai.internal.agentVersion"]; ok { + return result + } + + return "" +} + +// Agent version. Used to indicate the version of StatusMonitor installed on +// the computer if it is used for data collection. +func (tags InternalContextTags) SetAgentVersion(value string) { + if value != "" { + tags["ai.internal.agentVersion"] = value + } else { + delete(tags, "ai.internal.agentVersion") + } +} + +// This is the node name used for billing purposes. Use it to override the +// standard detection of nodes. +func (tags InternalContextTags) GetNodeName() string { + if result, ok := tags["ai.internal.nodeName"]; ok { + return result + } + + return "" +} + +// This is the node name used for billing purposes. Use it to override the +// standard detection of nodes. +func (tags InternalContextTags) SetNodeName(value string) { + if value != "" { + tags["ai.internal.nodeName"] = value + } else { + delete(tags, "ai.internal.nodeName") + } +} diff --git a/application-insights/appinsights/contracts/data.go b/application-insights/appinsights/contracts/data.go new file mode 100644 index 0000000000..144b7a8e50 --- /dev/null +++ b/application-insights/appinsights/contracts/data.go @@ -0,0 +1,25 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Data struct to contain both B and C sections. +type Data struct { + Base + + // Container for data item (B section). + BaseData interface{} `json:"baseData"` +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *Data) Sanitize() []string { + var warnings []string + + return warnings +} + +// Creates a new Data instance with default values set by the schema. +func NewData() *Data { + return &Data{} +} diff --git a/application-insights/appinsights/contracts/datapoint.go b/application-insights/appinsights/contracts/datapoint.go new file mode 100644 index 0000000000..b06beb1e5c --- /dev/null +++ b/application-insights/appinsights/contracts/datapoint.go @@ -0,0 +1,54 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Metric data single measurement. +type DataPoint struct { + + // Name of the metric. + Name string `json:"name"` + + // Metric type. Single measurement or the aggregated value. + Kind DataPointType `json:"kind"` + + // Single value for measurement. Sum of individual measurements for the + // aggregation. + Value float64 `json:"value"` + + // Metric weight of the aggregated metric. Should not be set for a + // measurement. + Count int `json:"count"` + + // Minimum value of the aggregated metric. Should not be set for a + // measurement. + Min float64 `json:"min"` + + // Maximum value of the aggregated metric. Should not be set for a + // measurement. + Max float64 `json:"max"` + + // Standard deviation of the aggregated metric. Should not be set for a + // measurement. + StdDev float64 `json:"stdDev"` +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *DataPoint) Sanitize() []string { + var warnings []string + + if len(data.Name) > 1024 { + data.Name = data.Name[:1024] + warnings = append(warnings, "DataPoint.Name exceeded maximum length of 1024") + } + + return warnings +} + +// Creates a new DataPoint instance with default values set by the schema. +func NewDataPoint() *DataPoint { + return &DataPoint{ + Kind: Measurement, + } +} diff --git a/application-insights/appinsights/contracts/datapointtype.go b/application-insights/appinsights/contracts/datapointtype.go new file mode 100644 index 0000000000..8f468e7a3c --- /dev/null +++ b/application-insights/appinsights/contracts/datapointtype.go @@ -0,0 +1,22 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Type of the metric data measurement. +type DataPointType int + +const ( + Measurement DataPointType = 0 + Aggregation DataPointType = 1 +) + +func (value DataPointType) String() string { + switch int(value) { + case 0: + return "Measurement" + case 1: + return "Aggregation" + default: + return "" + } +} diff --git a/application-insights/appinsights/contracts/domain.go b/application-insights/appinsights/contracts/domain.go new file mode 100644 index 0000000000..024945baec --- /dev/null +++ b/application-insights/appinsights/contracts/domain.go @@ -0,0 +1,21 @@ +package contracts + +// NOTE: This file was automatically generated. + +// The abstract common base of all domains. +type Domain struct { +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *Domain) Sanitize() []string { + var warnings []string + + return warnings +} + +// Creates a new Domain instance with default values set by the schema. +func NewDomain() *Domain { + return &Domain{} +} diff --git a/application-insights/appinsights/contracts/envelope.go b/application-insights/appinsights/contracts/envelope.go new file mode 100644 index 0000000000..91c80a9d5d --- /dev/null +++ b/application-insights/appinsights/contracts/envelope.go @@ -0,0 +1,82 @@ +package contracts + +// NOTE: This file was automatically generated. + +// System variables for a telemetry item. +type Envelope struct { + + // Envelope version. For internal use only. By assigning this the default, it + // will not be serialized within the payload unless changed to a value other + // than #1. + Ver int `json:"ver"` + + // Type name of telemetry data item. + Name string `json:"name"` + + // Event date time when telemetry item was created. This is the wall clock + // time on the client when the event was generated. There is no guarantee that + // the client's time is accurate. This field must be formatted in UTC ISO 8601 + // format, with a trailing 'Z' character, as described publicly on + // https://en.wikipedia.org/wiki/ISO_8601#UTC. Note: the number of decimal + // seconds digits provided are variable (and unspecified). Consumers should + // handle this, i.e. managed code consumers should not use format 'O' for + // parsing as it specifies a fixed length. Example: + // 2009-06-15T13:45:30.0000000Z. + Time string `json:"time"` + + // Sampling rate used in application. This telemetry item represents 1 / + // sampleRate actual telemetry items. + SampleRate float64 `json:"sampleRate"` + + // Sequence field used to track absolute order of uploaded events. + Seq string `json:"seq"` + + // The application's instrumentation key. The key is typically represented as + // a GUID, but there are cases when it is not a guid. No code should rely on + // iKey being a GUID. Instrumentation key is case insensitive. + IKey string `json:"iKey"` + + // Key/value collection of context properties. See ContextTagKeys for + // information on available properties. + Tags map[string]string `json:"tags,omitempty"` + + // Telemetry data item. + Data interface{} `json:"data"` +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *Envelope) Sanitize() []string { + var warnings []string + + if len(data.Name) > 1024 { + data.Name = data.Name[:1024] + warnings = append(warnings, "Envelope.Name exceeded maximum length of 1024") + } + + if len(data.Time) > 64 { + data.Time = data.Time[:64] + warnings = append(warnings, "Envelope.Time exceeded maximum length of 64") + } + + if len(data.Seq) > 64 { + data.Seq = data.Seq[:64] + warnings = append(warnings, "Envelope.Seq exceeded maximum length of 64") + } + + if len(data.IKey) > 40 { + data.IKey = data.IKey[:40] + warnings = append(warnings, "Envelope.IKey exceeded maximum length of 40") + } + + return warnings +} + +// Creates a new Envelope instance with default values set by the schema. +func NewEnvelope() *Envelope { + return &Envelope{ + Ver: 1, + SampleRate: 100.0, + } +} diff --git a/application-insights/appinsights/contracts/eventdata.go b/application-insights/appinsights/contracts/eventdata.go new file mode 100644 index 0000000000..2093c74fd0 --- /dev/null +++ b/application-insights/appinsights/contracts/eventdata.go @@ -0,0 +1,82 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Instances of Event represent structured event records that can be grouped +// and searched by their properties. Event data item also creates a metric of +// event count by name. +type EventData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // Event name. Keep it low cardinality to allow proper grouping and useful + // metrics. + Name string `json:"name"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` + + // Collection of custom measurements. + Measurements map[string]float64 `json:"measurements,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *EventData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".Event" + } else { + return "Microsoft.ApplicationInsights.Event" + } +} + +// Returns the base type when placed within a Data object container. +func (data *EventData) BaseType() string { + return "EventData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *EventData) Sanitize() []string { + var warnings []string + + if len(data.Name) > 512 { + data.Name = data.Name[:512] + warnings = append(warnings, "EventData.Name exceeded maximum length of 512") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "EventData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "EventData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + if data.Measurements != nil { + for k, v := range data.Measurements { + if len(k) > 150 { + data.Measurements[k[:150]] = v + delete(data.Measurements, k) + warnings = append(warnings, "EventData.Measurements has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new EventData instance with default values set by the schema. +func NewEventData() *EventData { + return &EventData{ + Ver: 2, + } +} diff --git a/application-insights/appinsights/contracts/exceptiondata.go b/application-insights/appinsights/contracts/exceptiondata.go new file mode 100644 index 0000000000..fe1c2f2b8e --- /dev/null +++ b/application-insights/appinsights/contracts/exceptiondata.go @@ -0,0 +1,93 @@ +package contracts + +// NOTE: This file was automatically generated. + +// An instance of Exception represents a handled or unhandled exception that +// occurred during execution of the monitored application. +type ExceptionData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // Exception chain - list of inner exceptions. + Exceptions []*ExceptionDetails `json:"exceptions"` + + // Severity level. Mostly used to indicate exception severity level when it is + // reported by logging library. + SeverityLevel SeverityLevel `json:"severityLevel"` + + // Identifier of where the exception was thrown in code. Used for exceptions + // grouping. Typically a combination of exception type and a function from the + // call stack. + ProblemId string `json:"problemId"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` + + // Collection of custom measurements. + Measurements map[string]float64 `json:"measurements,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *ExceptionData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".Exception" + } else { + return "Microsoft.ApplicationInsights.Exception" + } +} + +// Returns the base type when placed within a Data object container. +func (data *ExceptionData) BaseType() string { + return "ExceptionData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *ExceptionData) Sanitize() []string { + var warnings []string + + for _, ptr := range data.Exceptions { + warnings = append(warnings, ptr.Sanitize()...) + } + + if len(data.ProblemId) > 1024 { + data.ProblemId = data.ProblemId[:1024] + warnings = append(warnings, "ExceptionData.ProblemId exceeded maximum length of 1024") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "ExceptionData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "ExceptionData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + if data.Measurements != nil { + for k, v := range data.Measurements { + if len(k) > 150 { + data.Measurements[k[:150]] = v + delete(data.Measurements, k) + warnings = append(warnings, "ExceptionData.Measurements has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new ExceptionData instance with default values set by the schema. +func NewExceptionData() *ExceptionData { + return &ExceptionData{ + Ver: 2, + } +} diff --git a/application-insights/appinsights/contracts/exceptiondetails.go b/application-insights/appinsights/contracts/exceptiondetails.go new file mode 100644 index 0000000000..8b768ab6cf --- /dev/null +++ b/application-insights/appinsights/contracts/exceptiondetails.go @@ -0,0 +1,66 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Exception details of the exception in a chain. +type ExceptionDetails struct { + + // In case exception is nested (outer exception contains inner one), the id + // and outerId properties are used to represent the nesting. + Id int `json:"id"` + + // The value of outerId is a reference to an element in ExceptionDetails that + // represents the outer exception + OuterId int `json:"outerId"` + + // Exception type name. + TypeName string `json:"typeName"` + + // Exception message. + Message string `json:"message"` + + // Indicates if full exception stack is provided in the exception. The stack + // may be trimmed, such as in the case of a StackOverflow exception. + HasFullStack bool `json:"hasFullStack"` + + // Text describing the stack. Either stack or parsedStack should have a value. + Stack string `json:"stack"` + + // List of stack frames. Either stack or parsedStack should have a value. + ParsedStack []*StackFrame `json:"parsedStack,omitempty"` +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *ExceptionDetails) Sanitize() []string { + var warnings []string + + if len(data.TypeName) > 1024 { + data.TypeName = data.TypeName[:1024] + warnings = append(warnings, "ExceptionDetails.TypeName exceeded maximum length of 1024") + } + + if len(data.Message) > 32768 { + data.Message = data.Message[:32768] + warnings = append(warnings, "ExceptionDetails.Message exceeded maximum length of 32768") + } + + if len(data.Stack) > 32768 { + data.Stack = data.Stack[:32768] + warnings = append(warnings, "ExceptionDetails.Stack exceeded maximum length of 32768") + } + + for _, ptr := range data.ParsedStack { + warnings = append(warnings, ptr.Sanitize()...) + } + + return warnings +} + +// Creates a new ExceptionDetails instance with default values set by the schema. +func NewExceptionDetails() *ExceptionDetails { + return &ExceptionDetails{ + HasFullStack: true, + } +} diff --git a/application-insights/appinsights/contracts/messagedata.go b/application-insights/appinsights/contracts/messagedata.go new file mode 100644 index 0000000000..c0676431f2 --- /dev/null +++ b/application-insights/appinsights/contracts/messagedata.go @@ -0,0 +1,72 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Instances of Message represent printf-like trace statements that are +// text-searched. Log4Net, NLog and other text-based log file entries are +// translated into intances of this type. The message does not have +// measurements. +type MessageData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // Trace message + Message string `json:"message"` + + // Trace severity level. + SeverityLevel SeverityLevel `json:"severityLevel"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *MessageData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".Message" + } else { + return "Microsoft.ApplicationInsights.Message" + } +} + +// Returns the base type when placed within a Data object container. +func (data *MessageData) BaseType() string { + return "MessageData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *MessageData) Sanitize() []string { + var warnings []string + + if len(data.Message) > 32768 { + data.Message = data.Message[:32768] + warnings = append(warnings, "MessageData.Message exceeded maximum length of 32768") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "MessageData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "MessageData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new MessageData instance with default values set by the schema. +func NewMessageData() *MessageData { + return &MessageData{ + Ver: 2, + } +} diff --git a/application-insights/appinsights/contracts/metricdata.go b/application-insights/appinsights/contracts/metricdata.go new file mode 100644 index 0000000000..106576f2c7 --- /dev/null +++ b/application-insights/appinsights/contracts/metricdata.go @@ -0,0 +1,68 @@ +package contracts + +// NOTE: This file was automatically generated. + +// An instance of the Metric item is a list of measurements (single data +// points) and/or aggregations. +type MetricData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // List of metrics. Only one metric in the list is currently supported by + // Application Insights storage. If multiple data points were sent only the + // first one will be used. + Metrics []*DataPoint `json:"metrics"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *MetricData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".Metric" + } else { + return "Microsoft.ApplicationInsights.Metric" + } +} + +// Returns the base type when placed within a Data object container. +func (data *MetricData) BaseType() string { + return "MetricData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *MetricData) Sanitize() []string { + var warnings []string + + for _, ptr := range data.Metrics { + warnings = append(warnings, ptr.Sanitize()...) + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "MetricData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "MetricData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new MetricData instance with default values set by the schema. +func NewMetricData() *MetricData { + return &MetricData{ + Ver: 2, + } +} diff --git a/application-insights/appinsights/contracts/package.go b/application-insights/appinsights/contracts/package.go new file mode 100644 index 0000000000..ac96d6d35e --- /dev/null +++ b/application-insights/appinsights/contracts/package.go @@ -0,0 +1,4 @@ +// Data contract definitions for telemetry submitted to Application Insights. +// This is generated from the schemas found at +// https://github.com/microsoft/ApplicationInsights-Home/tree/master/EndpointSpecs/Schemas/Bond +package contracts diff --git a/application-insights/appinsights/contracts/pageviewdata.go b/application-insights/appinsights/contracts/pageviewdata.go new file mode 100644 index 0000000000..15e1d0aa93 --- /dev/null +++ b/application-insights/appinsights/contracts/pageviewdata.go @@ -0,0 +1,85 @@ +package contracts + +// NOTE: This file was automatically generated. + +// An instance of PageView represents a generic action on a page like a button +// click. It is also the base type for PageView. +type PageViewData struct { + Domain + EventData + + // Request URL with all query string parameters + Url string `json:"url"` + + // Request duration in format: DD.HH:MM:SS.MMMMMM. For a page view + // (PageViewData), this is the duration. For a page view with performance + // information (PageViewPerfData), this is the page load time. Must be less + // than 1000 days. + Duration string `json:"duration"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *PageViewData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".PageView" + } else { + return "Microsoft.ApplicationInsights.PageView" + } +} + +// Returns the base type when placed within a Data object container. +func (data *PageViewData) BaseType() string { + return "PageViewData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *PageViewData) Sanitize() []string { + var warnings []string + + if len(data.Url) > 2048 { + data.Url = data.Url[:2048] + warnings = append(warnings, "PageViewData.Url exceeded maximum length of 2048") + } + + if len(data.Name) > 512 { + data.Name = data.Name[:512] + warnings = append(warnings, "PageViewData.Name exceeded maximum length of 512") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "PageViewData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "PageViewData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + if data.Measurements != nil { + for k, v := range data.Measurements { + if len(k) > 150 { + data.Measurements[k[:150]] = v + delete(data.Measurements, k) + warnings = append(warnings, "PageViewData.Measurements has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new PageViewData instance with default values set by the schema. +func NewPageViewData() *PageViewData { + return &PageViewData{ + EventData: EventData{ + Ver: 2, + }, + } +} diff --git a/application-insights/appinsights/contracts/remotedependencydata.go b/application-insights/appinsights/contracts/remotedependencydata.go new file mode 100644 index 0000000000..f078243f45 --- /dev/null +++ b/application-insights/appinsights/contracts/remotedependencydata.go @@ -0,0 +1,134 @@ +package contracts + +// NOTE: This file was automatically generated. + +// An instance of Remote Dependency represents an interaction of the monitored +// component with a remote component/service like SQL or an HTTP endpoint. +type RemoteDependencyData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // Name of the command initiated with this dependency call. Low cardinality + // value. Examples are stored procedure name and URL path template. + Name string `json:"name"` + + // Identifier of a dependency call instance. Used for correlation with the + // request telemetry item corresponding to this dependency call. + Id string `json:"id"` + + // Result code of a dependency call. Examples are SQL error code and HTTP + // status code. + ResultCode string `json:"resultCode"` + + // Request duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 + // days. + Duration string `json:"duration"` + + // Indication of successfull or unsuccessfull call. + Success bool `json:"success"` + + // Command initiated by this dependency call. Examples are SQL statement and + // HTTP URL's with all query parameters. + Data string `json:"data"` + + // Target site of a dependency call. Examples are server name, host address. + Target string `json:"target"` + + // Dependency type name. Very low cardinality value for logical grouping of + // dependencies and interpretation of other fields like commandName and + // resultCode. Examples are SQL, Azure table, and HTTP. + Type string `json:"type"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` + + // Collection of custom measurements. + Measurements map[string]float64 `json:"measurements,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *RemoteDependencyData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".RemoteDependency" + } else { + return "Microsoft.ApplicationInsights.RemoteDependency" + } +} + +// Returns the base type when placed within a Data object container. +func (data *RemoteDependencyData) BaseType() string { + return "RemoteDependencyData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *RemoteDependencyData) Sanitize() []string { + var warnings []string + + if len(data.Name) > 1024 { + data.Name = data.Name[:1024] + warnings = append(warnings, "RemoteDependencyData.Name exceeded maximum length of 1024") + } + + if len(data.Id) > 128 { + data.Id = data.Id[:128] + warnings = append(warnings, "RemoteDependencyData.Id exceeded maximum length of 128") + } + + if len(data.ResultCode) > 1024 { + data.ResultCode = data.ResultCode[:1024] + warnings = append(warnings, "RemoteDependencyData.ResultCode exceeded maximum length of 1024") + } + + if len(data.Data) > 8192 { + data.Data = data.Data[:8192] + warnings = append(warnings, "RemoteDependencyData.Data exceeded maximum length of 8192") + } + + if len(data.Target) > 1024 { + data.Target = data.Target[:1024] + warnings = append(warnings, "RemoteDependencyData.Target exceeded maximum length of 1024") + } + + if len(data.Type) > 1024 { + data.Type = data.Type[:1024] + warnings = append(warnings, "RemoteDependencyData.Type exceeded maximum length of 1024") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "RemoteDependencyData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "RemoteDependencyData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + if data.Measurements != nil { + for k, v := range data.Measurements { + if len(k) > 150 { + data.Measurements[k[:150]] = v + delete(data.Measurements, k) + warnings = append(warnings, "RemoteDependencyData.Measurements has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new RemoteDependencyData instance with default values set by the schema. +func NewRemoteDependencyData() *RemoteDependencyData { + return &RemoteDependencyData{ + Ver: 2, + Success: true, + } +} diff --git a/application-insights/appinsights/contracts/requestdata.go b/application-insights/appinsights/contracts/requestdata.go new file mode 100644 index 0000000000..7db3b0aa90 --- /dev/null +++ b/application-insights/appinsights/contracts/requestdata.go @@ -0,0 +1,125 @@ +package contracts + +// NOTE: This file was automatically generated. + +// An instance of Request represents completion of an external request to the +// application to do work and contains a summary of that request execution and +// the results. +type RequestData struct { + Domain + + // Schema version + Ver int `json:"ver"` + + // Identifier of a request call instance. Used for correlation between request + // and other telemetry items. + Id string `json:"id"` + + // Source of the request. Examples are the instrumentation key of the caller + // or the ip address of the caller. + Source string `json:"source"` + + // Name of the request. Represents code path taken to process request. Low + // cardinality value to allow better grouping of requests. For HTTP requests + // it represents the HTTP method and URL path template like 'GET + // /values/{id}'. + Name string `json:"name"` + + // Request duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 + // days. + Duration string `json:"duration"` + + // Result of a request execution. HTTP status code for HTTP requests. + ResponseCode string `json:"responseCode"` + + // Indication of successfull or unsuccessfull call. + Success bool `json:"success"` + + // Request URL with all query string parameters. + Url string `json:"url"` + + // Collection of custom properties. + Properties map[string]string `json:"properties,omitempty"` + + // Collection of custom measurements. + Measurements map[string]float64 `json:"measurements,omitempty"` +} + +// Returns the name used when this is embedded within an Envelope container. +func (data *RequestData) EnvelopeName(key string) string { + if key != "" { + return "Microsoft.ApplicationInsights." + key + ".Request" + } else { + return "Microsoft.ApplicationInsights.Request" + } +} + +// Returns the base type when placed within a Data object container. +func (data *RequestData) BaseType() string { + return "RequestData" +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *RequestData) Sanitize() []string { + var warnings []string + + if len(data.Id) > 128 { + data.Id = data.Id[:128] + warnings = append(warnings, "RequestData.Id exceeded maximum length of 128") + } + + if len(data.Source) > 1024 { + data.Source = data.Source[:1024] + warnings = append(warnings, "RequestData.Source exceeded maximum length of 1024") + } + + if len(data.Name) > 1024 { + data.Name = data.Name[:1024] + warnings = append(warnings, "RequestData.Name exceeded maximum length of 1024") + } + + if len(data.ResponseCode) > 1024 { + data.ResponseCode = data.ResponseCode[:1024] + warnings = append(warnings, "RequestData.ResponseCode exceeded maximum length of 1024") + } + + if len(data.Url) > 2048 { + data.Url = data.Url[:2048] + warnings = append(warnings, "RequestData.Url exceeded maximum length of 2048") + } + + if data.Properties != nil { + for k, v := range data.Properties { + if len(v) > 8192 { + data.Properties[k] = v[:8192] + warnings = append(warnings, "RequestData.Properties has value with length exceeding max of 8192: "+k) + } + if len(k) > 150 { + data.Properties[k[:150]] = data.Properties[k] + delete(data.Properties, k) + warnings = append(warnings, "RequestData.Properties has key with length exceeding max of 150: "+k) + } + } + } + + if data.Measurements != nil { + for k, v := range data.Measurements { + if len(k) > 150 { + data.Measurements[k[:150]] = v + delete(data.Measurements, k) + warnings = append(warnings, "RequestData.Measurements has key with length exceeding max of 150: "+k) + } + } + } + + return warnings +} + +// Creates a new RequestData instance with default values set by the schema. +func NewRequestData() *RequestData { + return &RequestData{ + Ver: 2, + } +} diff --git a/application-insights/appinsights/contracts/severitylevel.go b/application-insights/appinsights/contracts/severitylevel.go new file mode 100644 index 0000000000..a2ec9b8f03 --- /dev/null +++ b/application-insights/appinsights/contracts/severitylevel.go @@ -0,0 +1,31 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Defines the level of severity for the event. +type SeverityLevel int + +const ( + Verbose SeverityLevel = 0 + Information SeverityLevel = 1 + Warning SeverityLevel = 2 + Error SeverityLevel = 3 + Critical SeverityLevel = 4 +) + +func (value SeverityLevel) String() string { + switch int(value) { + case 0: + return "Verbose" + case 1: + return "Information" + case 2: + return "Warning" + case 3: + return "Error" + case 4: + return "Critical" + default: + return "" + } +} diff --git a/application-insights/appinsights/contracts/stackframe.go b/application-insights/appinsights/contracts/stackframe.go new file mode 100644 index 0000000000..d012f6b140 --- /dev/null +++ b/application-insights/appinsights/contracts/stackframe.go @@ -0,0 +1,52 @@ +package contracts + +// NOTE: This file was automatically generated. + +// Stack frame information. +type StackFrame struct { + + // Level in the call stack. For the long stacks SDK may not report every + // function in a call stack. + Level int `json:"level"` + + // Method name. + Method string `json:"method"` + + // Name of the assembly (dll, jar, etc.) containing this function. + Assembly string `json:"assembly"` + + // File name or URL of the method implementation. + FileName string `json:"fileName"` + + // Line number of the code implementation. + Line int `json:"line"` +} + +// Truncates string fields that exceed their maximum supported sizes for this +// object and all objects it references. Returns a warning for each affected +// field. +func (data *StackFrame) Sanitize() []string { + var warnings []string + + if len(data.Method) > 1024 { + data.Method = data.Method[:1024] + warnings = append(warnings, "StackFrame.Method exceeded maximum length of 1024") + } + + if len(data.Assembly) > 1024 { + data.Assembly = data.Assembly[:1024] + warnings = append(warnings, "StackFrame.Assembly exceeded maximum length of 1024") + } + + if len(data.FileName) > 1024 { + data.FileName = data.FileName[:1024] + warnings = append(warnings, "StackFrame.FileName exceeded maximum length of 1024") + } + + return warnings +} + +// Creates a new StackFrame instance with default values set by the schema. +func NewStackFrame() *StackFrame { + return &StackFrame{} +} diff --git a/application-insights/appinsights/diagnostics.go b/application-insights/appinsights/diagnostics.go new file mode 100644 index 0000000000..7ff90bfaee --- /dev/null +++ b/application-insights/appinsights/diagnostics.go @@ -0,0 +1,88 @@ +package appinsights + +import ( + "fmt" + "sync" +) + +type diagnosticsMessageWriter struct { + listeners []*diagnosticsMessageListener + lock sync.Mutex +} + +// Handler function for receiving diagnostics messages. If this returns an +// error, then the listener will be removed. +type DiagnosticsMessageHandler func(string) error + +// Listener type returned by NewDiagnosticsMessageListener. +type DiagnosticsMessageListener interface { + // Stop receiving diagnostics messages from this listener. + Remove() +} + +type diagnosticsMessageListener struct { + handler DiagnosticsMessageHandler + writer *diagnosticsMessageWriter +} + +func (listener *diagnosticsMessageListener) Remove() { + listener.writer.removeListener(listener) +} + +// The one and only diagnostics writer. +var diagnosticsWriter = &diagnosticsMessageWriter{} + +// Subscribes the specified handler to diagnostics messages from the SDK. The +// returned interface can be used to unsubscribe. +func NewDiagnosticsMessageListener(handler DiagnosticsMessageHandler) DiagnosticsMessageListener { + listener := &diagnosticsMessageListener{ + handler: handler, + writer: diagnosticsWriter, + } + + diagnosticsWriter.appendListener(listener) + return listener +} + +func (writer *diagnosticsMessageWriter) appendListener(listener *diagnosticsMessageListener) { + writer.lock.Lock() + defer writer.lock.Unlock() + writer.listeners = append(writer.listeners, listener) +} + +func (writer *diagnosticsMessageWriter) removeListener(listener *diagnosticsMessageListener) { + writer.lock.Lock() + defer writer.lock.Unlock() + + for i := 0; i < len(writer.listeners); i++ { + if writer.listeners[i] == listener { + writer.listeners[i] = writer.listeners[len(writer.listeners)-1] + writer.listeners = writer.listeners[:len(writer.listeners)-1] + return + } + } +} + +func (writer *diagnosticsMessageWriter) Write(message string) { + var toRemove []*diagnosticsMessageListener + for _, listener := range writer.listeners { + if err := listener.handler(message); err != nil { + toRemove = append(toRemove, listener) + } + } + + for _, listener := range toRemove { + listener.Remove() + } +} + +func (writer *diagnosticsMessageWriter) Printf(message string, args ...interface{}) { + // Don't bother with Sprintf if nobody is listening + if writer.hasListeners() { + writer.Write(fmt.Sprintf(message, args...)) + } +} + +func (writer *diagnosticsMessageWriter) hasListeners() bool { + return len(writer.listeners) > 0 +} diff --git a/application-insights/appinsights/diagnostics_test.go b/application-insights/appinsights/diagnostics_test.go new file mode 100644 index 0000000000..d5f741d51c --- /dev/null +++ b/application-insights/appinsights/diagnostics_test.go @@ -0,0 +1,120 @@ +package appinsights + +import ( + "fmt" + "testing" + "time" +) + +func TestMessageSentToConsumers(t *testing.T) { + original := "~~~test_message~~~" + + // There may be spurious messages sent by a transmitter's goroutine from another test, + // so just check that we do get the test message *at some point*. + + listener1chan := make(chan bool, 1) + NewDiagnosticsMessageListener(func(message string) error { + if message == original { + listener1chan <- true + } + + return nil + }) + + listener2chan := make(chan bool, 1) + NewDiagnosticsMessageListener(func(message string) error { + if message == original { + listener2chan <- true + } + + return nil + }) + + defer resetDiagnosticsListeners() + diagnosticsWriter.Write(original) + + listener1recvd := false + listener2recvd := false + timeout := false + timer := time.After(time.Second) + for !(listener1recvd && listener2recvd) && !timeout { + select { + case <-listener1chan: + listener1recvd = true + case <-listener2chan: + listener2recvd = true + case <-timer: + timeout = true + } + } + + if timeout { + t.Errorf("Message failed to be delivered to both listeners") + } +} + +func TestRemoveListener(t *testing.T) { + mchan := make(chan string, 1) + listener := NewDiagnosticsMessageListener(func(message string) error { + mchan <- message + return nil + }) + + defer resetDiagnosticsListeners() + + diagnosticsWriter.Write("Hello") + select { + case <-mchan: + default: + t.Fatalf("Message not received") + } + + listener.Remove() + + diagnosticsWriter.Write("Hello") + select { + case <-mchan: + t.Fatalf("Message received after remove") + default: + } +} + +func TestErroredListenerIsRemoved(t *testing.T) { + mchan := make(chan string, 1) + echan := make(chan error, 1) + NewDiagnosticsMessageListener(func(message string) error { + mchan <- message + return <-echan + }) + defer resetDiagnosticsListeners() + + echan <- nil + diagnosticsWriter.Write("Hello") + select { + case <-mchan: + default: + t.Fatal("Message not received") + } + + echan <- fmt.Errorf("Test error") + diagnosticsWriter.Write("Hello") + select { + case <-mchan: + default: + t.Fatal("Message not received") + } + + echan <- nil + diagnosticsWriter.Write("Not received") + select { + case <-mchan: + t.Fatalf("Message received after error") + default: + } +} + +func resetDiagnosticsListeners() { + diagnosticsWriter.lock.Lock() + defer diagnosticsWriter.lock.Unlock() + diagnosticsWriter.listeners = diagnosticsWriter.listeners[:0] +} diff --git a/application-insights/appinsights/exception.go b/application-insights/appinsights/exception.go new file mode 100644 index 0000000000..c440797dcf --- /dev/null +++ b/application-insights/appinsights/exception.go @@ -0,0 +1,150 @@ +package appinsights + +import ( + "fmt" + "reflect" + "runtime" + "strings" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +// Exception telemetry items represent a handled or unhandled exceptions that +// occurred during execution of the monitored application. +type ExceptionTelemetry struct { + BaseTelemetry + BaseTelemetryMeasurements + + // Panic message: string, error, or Stringer + Error interface{} + + // List of stack frames. Use GetCallstack to generate this data. + Frames []*contracts.StackFrame + + // Severity level. + SeverityLevel contracts.SeverityLevel +} + +// Creates a new exception telemetry item with the specified error and the +// current callstack. This should be used directly from a function that +// handles a recover(), or to report an unexpected error return value from +// a function. +func NewExceptionTelemetry(err interface{}) *ExceptionTelemetry { + return newExceptionTelemetry(err, 1) +} + +func newExceptionTelemetry(err interface{}, skip int) *ExceptionTelemetry { + return &ExceptionTelemetry{ + Error: err, + Frames: GetCallstack(2 + skip), + SeverityLevel: Error, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + BaseTelemetryMeasurements: BaseTelemetryMeasurements{ + Measurements: make(map[string]float64), + }, + } +} + +func (telem *ExceptionTelemetry) TelemetryData() TelemetryData { + details := contracts.NewExceptionDetails() + details.HasFullStack = len(telem.Frames) > 0 + details.ParsedStack = telem.Frames + + if err, ok := telem.Error.(error); ok { + details.Message = err.Error() + details.TypeName = reflect.TypeOf(telem.Error).String() + } else if str, ok := telem.Error.(string); ok { + details.Message = str + details.TypeName = "string" + } else if stringer, ok := telem.Error.(fmt.Stringer); ok { + details.Message = stringer.String() + details.TypeName = reflect.TypeOf(telem.Error).String() + } else if stringer, ok := telem.Error.(fmt.GoStringer); ok { + details.Message = stringer.GoString() + details.TypeName = reflect.TypeOf(telem.Error).String() + } else { + details.Message = "" + details.TypeName = "" + } + + data := contracts.NewExceptionData() + data.SeverityLevel = telem.SeverityLevel + data.Exceptions = []*contracts.ExceptionDetails{details} + data.Properties = telem.Properties + data.Measurements = telem.Measurements + + return data +} + +// Generates a callstack suitable for inclusion in Application Insights +// exception telemetry for the current goroutine, skipping a number of frames +// specified by skip. +func GetCallstack(skip int) []*contracts.StackFrame { + var stackFrames []*contracts.StackFrame + + if skip < 0 { + skip = 0 + } + + stack := make([]uintptr, 64+skip) + depth := runtime.Callers(skip+1, stack) + if depth == 0 { + return stackFrames + } + + frames := runtime.CallersFrames(stack[:depth]) + level := 0 + for { + frame, more := frames.Next() + + stackFrame := &contracts.StackFrame{ + Level: level, + FileName: frame.File, + Line: frame.Line, + } + + if frame.Function != "" { + /* Default */ + stackFrame.Method = frame.Function + + /* Break up function into assembly/function */ + lastSlash := strings.LastIndexByte(frame.Function, '/') + if lastSlash < 0 { + // e.g. "runtime.gopanic" + // The below works with lastSlash=0 + lastSlash = 0 + } + + firstDot := strings.IndexByte(frame.Function[lastSlash:], '.') + if firstDot >= 0 { + stackFrame.Assembly = frame.Function[:lastSlash+firstDot] + stackFrame.Method = frame.Function[lastSlash+firstDot+1:] + } + } + + stackFrames = append(stackFrames, stackFrame) + + level++ + if !more { + break + } + } + + return stackFrames +} + +// Recovers from any active panics and tracks them to the specified +// TelemetryClient. If rethrow is set to true, then this will panic. +// Should be invoked via defer in functions to monitor. +func TrackPanic(client TelemetryClient, rethrow bool) { + if r := recover(); r != nil { + client.Track(newExceptionTelemetry(r, 1)) + if rethrow { + panic(r) + } + } +} diff --git a/application-insights/appinsights/exception_test.go b/application-insights/appinsights/exception_test.go new file mode 100644 index 0000000000..c42b91d1f9 --- /dev/null +++ b/application-insights/appinsights/exception_test.go @@ -0,0 +1,193 @@ +package appinsights + +import ( + "fmt" + "strings" + "testing" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +type myStringer struct{} + +func (s *myStringer) String() string { + return "My stringer error" +} + +type myError struct{} + +func (s *myError) Error() string { + return "My error error" +} + +type myGoStringer struct{} + +func (s *myGoStringer) Error() string { + return "My go stringer error" +} + +func TestExceptionTelemetry(t *testing.T) { + // Test callstack capture -- these should all fit in 64 frames. + for i := 9; i < 20; i++ { + exd := testExceptionCallstack(t, i) + checkDataContract(t, "ExceptionDetails.TypeName", exd.TypeName, "string") + checkDataContract(t, "ExceptionDetails.Message", exd.Message, "Whoops") + checkDataContract(t, "ExceptionDetails.HasFullStack", exd.HasFullStack, true) + } + + // Test error types + var err error + err = &myError{} + + e1 := catchPanic(err) + exd1 := e1.TelemetryData().(*contracts.ExceptionData).Exceptions[0] + checkDataContract(t, "ExceptionDetails.Message", exd1.Message, "My error error") + checkDataContract(t, "ExceptionDetails.TypeName", exd1.TypeName, "*appinsights.myError") + + e2 := catchPanic(&myStringer{}) + exd2 := e2.TelemetryData().(*contracts.ExceptionData).Exceptions[0] + checkDataContract(t, "ExceptionDetails.Message", exd2.Message, "My stringer error") + checkDataContract(t, "ExceptionDetails.TypeName", exd2.TypeName, "*appinsights.myStringer") + + e3 := catchPanic(&myGoStringer{}) + exd3 := e3.TelemetryData().(*contracts.ExceptionData).Exceptions[0] + checkDataContract(t, "ExceptionDetails.Message", exd3.Message, "My go stringer error") + checkDataContract(t, "ExceptionDetails.TypeName", exd3.TypeName, "*appinsights.myGoStringer") +} + +func TestTrackPanic(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + + catchTrackPanic(client, "~exception~") + client.Channel().Close() + + req := transmitter.waitForRequest(t) + if !strings.Contains(req.payload, "~exception~") { + t.Error("Unexpected payload") + } +} + +func testExceptionCallstack(t *testing.T, n int) *contracts.ExceptionDetails { + d := buildStack(n).TelemetryData().(*contracts.ExceptionData) + checkDataContract(t, "len(Exceptions)", len(d.Exceptions), 1) + ex := d.Exceptions[0] + + // Find the relevant range of frames + frstart := -1 + frend := -1 + for i, f := range ex.ParsedStack { + if strings.Contains(f.Method, "Collatz") { + if frstart < 0 { + frstart = i + } + } else { + if frstart >= 0 && frend < 0 { + frend = i + break + } + } + } + + expected := collatzFrames(n) + + if frend-frstart != len(expected) { + t.Errorf("Wrong number of Collatz frames found. Got %d, want %d.", frend-frstart, len(expected)) + return ex + } + + j := len(expected) - 1 + for i := frstart; j >= 0 && i < len(ex.ParsedStack); i++ { + checkDataContract(t, fmt.Sprintf("ParsedStack[%d].Method", i), ex.ParsedStack[i].Method, expected[j]) + if !strings.HasSuffix(ex.ParsedStack[i].Assembly, "/ApplicationInsights-Go/appinsights") { + checkDataContract(t, fmt.Sprintf("ParsedStack[%d].Assembly", i), ex.ParsedStack[i].Assembly, "/ApplicationInsights-Go/appinsights") + } + if !strings.HasSuffix(ex.ParsedStack[i].FileName, "/exception_test.go") { + checkDataContract(t, fmt.Sprintf("ParsedStack[%d].FileName", i), ex.ParsedStack[i].FileName, "exception_test.go") + } + + j-- + } + + return ex +} + +func collatzFrames(n int) []string { + var result []string + + result = append(result, "panicTestCollatz") + for n != 1 { + if (n % 2) == 0 { + result = append(result, "panicTestCollatzEven") + n /= 2 + } else { + result = append(result, "panicTestCollatzOdd") + n = 1 + (3 * n) + } + + result = append(result, "panicTestCollatz") + } + + return result +} + +func catchPanic(err interface{}) *ExceptionTelemetry { + var result *ExceptionTelemetry + + func() { + defer func() { + if err := recover(); err != nil { + result = NewExceptionTelemetry(err) + } + }() + + panic(err) + }() + + return result +} + +func buildStack(n int) *ExceptionTelemetry { + var result *ExceptionTelemetry + + // Nest this so the panic doesn't supercede the return + func() { + defer func() { + if err := recover(); err != nil { + result = NewExceptionTelemetry(err) + } + }() + + panicTestCollatz(n) + }() + + return result +} + +// Test Collatz's conjecture for a given input; panic in the base case. +func panicTestCollatz(n int) int { + if n == 1 { + panic("Whoops") + } + + if (n & 1) == 0 { + return panicTestCollatzEven(n) + } else { + return panicTestCollatzOdd(n) + } +} + +func panicTestCollatzEven(n int) int { + return panicTestCollatz(n / 2) +} + +func panicTestCollatzOdd(n int) int { + return panicTestCollatz((3 * n) + 1) +} + +func catchTrackPanic(client TelemetryClient, err interface{}) { + defer TrackPanic(client, false) + panic(err) +} diff --git a/application-insights/appinsights/inmemorychannel.go b/application-insights/appinsights/inmemorychannel.go new file mode 100644 index 0000000000..4296e4c899 --- /dev/null +++ b/application-insights/appinsights/inmemorychannel.go @@ -0,0 +1,449 @@ +package appinsights + +import ( + "sync" + "time" + + "code.cloudfoundry.org/clock" + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +var ( + submit_retries = []time.Duration{time.Duration(10 * time.Second), time.Duration(30 * time.Second), time.Duration(60 * time.Second)} +) + +// A telemetry channel that stores events exclusively in memory. Presently +// the only telemetry channel implementation available. +type InMemoryChannel struct { + endpointAddress string + isDeveloperMode bool + collectChan chan *contracts.Envelope + controlChan chan *inMemoryChannelControl + batchSize int + batchInterval time.Duration + waitgroup sync.WaitGroup + throttle *throttleManager + transmitter transmitter +} + +type inMemoryChannelControl struct { + // If true, flush the buffer. + flush bool + + // If true, stop listening on the channel. (Flush is required if any events are to be sent) + stop bool + + // If stopping and flushing, this specifies whether to retry submissions on error. + retry bool + + // If retrying, what is the max time to wait before finishing up? + timeout time.Duration + + // If specified, a message will be sent on this channel when all pending telemetry items have been submitted + callback chan struct{} +} + +// Creates an InMemoryChannel instance and starts a background submission +// goroutine. +func NewInMemoryChannel(config *TelemetryConfiguration) *InMemoryChannel { + channel := &InMemoryChannel{ + endpointAddress: config.EndpointUrl, + collectChan: make(chan *contracts.Envelope), + controlChan: make(chan *inMemoryChannelControl), + batchSize: config.MaxBatchSize, + batchInterval: config.MaxBatchInterval, + throttle: newThrottleManager(), + transmitter: newTransmitter(config.EndpointUrl, config.Client), + } + + go channel.acceptLoop() + + return channel +} + +// The address of the endpoint to which telemetry is sent +func (channel *InMemoryChannel) EndpointAddress() string { + return channel.endpointAddress +} + +// Queues a single telemetry item +func (channel *InMemoryChannel) Send(item *contracts.Envelope) { + if item != nil && channel.collectChan != nil { + channel.collectChan <- item + } +} + +// Forces the current queue to be sent +func (channel *InMemoryChannel) Flush() { + if channel.controlChan != nil { + channel.controlChan <- &inMemoryChannelControl{ + flush: true, + } + } +} + +// Tears down the submission goroutines, closes internal channels. Any +// telemetry waiting to be sent is discarded. Further calls to Send() have +// undefined behavior. This is a more abrupt version of Close(). +func (channel *InMemoryChannel) Stop() { + if channel.controlChan != nil { + channel.controlChan <- &inMemoryChannelControl{ + stop: true, + } + } +} + +// Returns true if this channel has been throttled by the data collector. +func (channel *InMemoryChannel) IsThrottled() bool { + return channel.throttle != nil && channel.throttle.IsThrottled() +} + +// Flushes and tears down the submission goroutine and closes internal +// channels. Returns a channel that is closed when all pending telemetry +// items have been submitted and it is safe to shut down without losing +// telemetry. +// +// If retryTimeout is specified and non-zero, then failed submissions will +// be retried until one succeeds or the timeout expires, whichever occurs +// first. A retryTimeout of zero indicates that failed submissions will be +// retried as usual. An omitted retryTimeout indicates that submissions +// should not be retried if they fail. +// +// Note that the returned channel may not be closed before retryTimeout even +// if it is specified. This is because retryTimeout only applies to the +// latest telemetry buffer. This may be typical for applications that +// submit a large amount of telemetry or are prone to being throttled. When +// exiting, you should select on the result channel and your own timer to +// avoid long delays. +func (channel *InMemoryChannel) Close(timeout ...time.Duration) <-chan struct{} { + if channel.controlChan != nil { + callback := make(chan struct{}) + + ctl := &inMemoryChannelControl{ + stop: true, + flush: true, + retry: false, + callback: callback, + } + + if len(timeout) > 0 { + ctl.retry = true + ctl.timeout = timeout[0] + } + + channel.controlChan <- ctl + + return callback + } else { + return nil + } +} + +func (channel *InMemoryChannel) acceptLoop() { + channelState := newInMemoryChannelState(channel) + + for !channelState.stopping { + channelState.start() + } + + channelState.stop() +} + +// Data shared between parts of a channel +type inMemoryChannelState struct { + channel *InMemoryChannel + stopping bool + buffer telemetryBufferItems + retry bool + retryTimeout time.Duration + callback chan struct{} + timer clock.Timer +} + +func newInMemoryChannelState(channel *InMemoryChannel) *inMemoryChannelState { + // Initialize timer to stopped -- avoid any chance of a race condition. + timer := currentClock.NewTimer(time.Hour) + timer.Stop() + + return &inMemoryChannelState{ + channel: channel, + buffer: make(telemetryBufferItems, 0, 16), + stopping: false, + timer: timer, + } +} + +// Part of channel accept loop: Initialize buffer and accept first message, handle controls. +func (state *inMemoryChannelState) start() bool { + if len(state.buffer) > 16 { + // Start out with the size of the previous buffer + state.buffer = make(telemetryBufferItems, 0, cap(state.buffer)) + } else if len(state.buffer) > 0 { + // Start out with at least 16 slots + state.buffer = make(telemetryBufferItems, 0, 16) + } + + // Wait for an event + select { + case event := <-state.channel.collectChan: + if event == nil { + // Channel closed? Not intercepted by Send()? + panic("Received nil event") + } + + state.buffer = append(state.buffer, event) + + case ctl := <-state.channel.controlChan: + // The buffer is empty, so there would be no point in flushing + state.channel.signalWhenDone(ctl.callback) + + if ctl.stop { + state.stopping = true + return false + } + } + + if len(state.buffer) == 0 { + return true + } + + return state.waitToSend() +} + +// Part of channel accept loop: Wait for buffer to fill, timeout to expire, or flush +func (state *inMemoryChannelState) waitToSend() bool { + // Things that are used by the sender if we receive a control message + state.retryTimeout = 0 + state.retry = true + state.callback = nil + + // Delay until timeout passes or buffer fills up + state.timer.Reset(state.channel.batchInterval) + + for { + if len(state.buffer) >= state.channel.batchSize { + if !state.timer.Stop() { + <-state.timer.C() + } + + return state.send() + } + + select { + case event := <-state.channel.collectChan: + if event == nil { + // Channel closed? Not intercepted by Send()? + panic("Received nil event") + } + + state.buffer = append(state.buffer, event) + + case ctl := <-state.channel.controlChan: + if ctl.stop { + state.stopping = true + state.retry = ctl.retry + if !ctl.flush { + // No flush? Just exit. + state.channel.signalWhenDone(ctl.callback) + return false + } + } + + if ctl.flush { + if !state.timer.Stop() { + <-state.timer.C() + } + + state.retryTimeout = ctl.timeout + state.callback = ctl.callback + return state.send() + } + + case <-state.timer.C(): + // Timeout expired + return state.send() + } + } +} + +// Part of channel accept loop: Check and wait on throttle, submit pending telemetry +func (state *inMemoryChannelState) send() bool { + // Hold up transmission if we're being throttled + if !state.stopping && state.channel.throttle.IsThrottled() { + if !state.waitThrottle() { + // Stopped + return false + } + } + + // Send + if len(state.buffer) > 0 { + state.channel.waitgroup.Add(1) + + // If we have a callback, wait on the waitgroup now that it's + // incremented. + state.channel.signalWhenDone(state.callback) + + go func(buffer telemetryBufferItems, retry bool, retryTimeout time.Duration) { + defer state.channel.waitgroup.Done() + state.channel.transmitRetry(buffer, retry, retryTimeout) + }(state.buffer, state.retry, state.retryTimeout) + } else if state.callback != nil { + state.channel.signalWhenDone(state.callback) + } + + return true +} + +// Part of channel accept loop: Wait for throttle to expire while dropping messages +func (state *inMemoryChannelState) waitThrottle() bool { + // Channel is currently throttled. Once the buffer fills, messages will + // be lost... If we're exiting, then we'll just try to submit anyway. That + // request may be throttled and transmitRetry will perform the backoff correctly. + + diagnosticsWriter.Write("Channel is throttled, events may be dropped.") + throttleDone := state.channel.throttle.NotifyWhenReady() + dropped := 0 + + defer diagnosticsWriter.Printf("Channel dropped %d events while throttled", dropped) + + for { + select { + case <-throttleDone: + close(throttleDone) + return true + + case event := <-state.channel.collectChan: + // If there's still room in the buffer, then go ahead and add it. + if len(state.buffer) < state.channel.batchSize { + state.buffer = append(state.buffer, event) + } else { + if dropped == 0 { + diagnosticsWriter.Write("Buffer is full, dropping further events.") + } + + dropped++ + } + + case ctl := <-state.channel.controlChan: + if ctl.stop { + state.stopping = true + state.retry = ctl.retry + if !ctl.flush { + state.channel.signalWhenDone(ctl.callback) + return false + } else { + // Make an exception when stopping + return true + } + } + + // Cannot flush + // TODO: Figure out what to do about callback? + if ctl.flush { + state.channel.signalWhenDone(ctl.callback) + } + } + } +} + +// Part of channel accept loop: Clean up and close telemetry channel +func (state *inMemoryChannelState) stop() { + close(state.channel.collectChan) + close(state.channel.controlChan) + + state.channel.collectChan = nil + state.channel.controlChan = nil + + // Throttle can't close until transmitters are done using it. + state.channel.waitgroup.Wait() + state.channel.throttle.Stop() + + state.channel.throttle = nil +} + +func (channel *InMemoryChannel) transmitRetry(items telemetryBufferItems, retry bool, retryTimeout time.Duration) { + payload := items.serialize() + retryTimeRemaining := retryTimeout + + for _, wait := range submit_retries { + result, err := channel.transmitter.Transmit(payload, items) + if err == nil && result != nil && result.IsSuccess() { + return + } + + if !retry { + diagnosticsWriter.Write("Refusing to retry telemetry submission (retry==false)") + return + } + + // Check for success, determine if we need to retry anything + if result != nil { + if result.CanRetry() { + // Filter down to failed items + payload, items = result.GetRetryItems(payload, items) + if len(payload) == 0 || len(items) == 0 { + return + } + } else { + diagnosticsWriter.Write("Cannot retry telemetry submission") + return + } + + // Check for throttling + if result.IsThrottled() { + if result.retryAfter != nil { + diagnosticsWriter.Printf("Channel is throttled until %s", *result.retryAfter) + channel.throttle.RetryAfter(*result.retryAfter) + } else { + // TODO: Pick a time + } + } + } + + if retryTimeout > 0 { + // We're on a time schedule here. Make sure we don't try longer + // than we have been allowed. + if retryTimeRemaining < wait { + // One more chance left -- we'll wait the max time we can + // and then retry on the way out. + currentClock.Sleep(retryTimeRemaining) + break + } else { + // Still have time left to go through the rest of the regular + // retry schedule + retryTimeRemaining -= wait + } + } + + diagnosticsWriter.Printf("Waiting %s to retry submission", wait) + currentClock.Sleep(wait) + + // Wait if the channel is throttled and we're not on a schedule + if channel.IsThrottled() && retryTimeout == 0 { + diagnosticsWriter.Printf("Channel is throttled; extending wait time.") + ch := channel.throttle.NotifyWhenReady() + result := <-ch + close(ch) + + if !result { + return + } + } + } + + // One final try + _, err := channel.transmitter.Transmit(payload, items) + if err != nil { + diagnosticsWriter.Write("Gave up transmitting payload; exhausted retries") + } +} + +func (channel *InMemoryChannel) signalWhenDone(callback chan struct{}) { + if callback != nil { + go func() { + channel.waitgroup.Wait() + close(callback) + }() + } +} diff --git a/application-insights/appinsights/inmemorychannel_test.go b/application-insights/appinsights/inmemorychannel_test.go new file mode 100644 index 0000000000..e02e11c699 --- /dev/null +++ b/application-insights/appinsights/inmemorychannel_test.go @@ -0,0 +1,628 @@ +package appinsights + +import ( + "fmt" + "strings" + "testing" + "time" +) + +const ten_seconds = time.Duration(10) * time.Second + +type testTransmitter struct { + requests chan *testTransmission + responses chan *transmissionResult +} + +func (transmitter *testTransmitter) Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) { + itemsCopy := make(telemetryBufferItems, len(items)) + copy(itemsCopy, items) + + transmitter.requests <- &testTransmission{ + payload: string(payload), + items: itemsCopy, + timestamp: currentClock.Now(), + } + + return <-transmitter.responses, nil +} + +func (transmitter *testTransmitter) Close() { + close(transmitter.requests) + close(transmitter.responses) +} + +func (transmitter *testTransmitter) prepResponse(statusCodes ...int) { + for _, code := range statusCodes { + transmitter.responses <- &transmissionResult{statusCode: code} + } +} + +func (transmitter *testTransmitter) prepThrottle(after time.Duration) time.Time { + retryAfter := currentClock.Now().Add(after) + + transmitter.responses <- &transmissionResult{ + statusCode: 408, + retryAfter: &retryAfter, + } + + return retryAfter +} + +func (transmitter *testTransmitter) waitForRequest(t *testing.T) *testTransmission { + select { + case req := <-transmitter.requests: + return req + case <-time.After(time.Duration(500) * time.Millisecond): + t.Fatal("Timed out waiting for request to be sent") + return nil /* Not reached */ + } +} + +func (transmitter *testTransmitter) assertNoRequest(t *testing.T) { + select { + case <-transmitter.requests: + t.Fatal("Expected no request") + case <-time.After(time.Duration(10) * time.Millisecond): + return + } +} + +type testTransmission struct { + timestamp time.Time + payload string + items telemetryBufferItems +} + +func newTestChannelServer(config ...*TelemetryConfiguration) (TelemetryClient, *testTransmitter) { + transmitter := &testTransmitter{ + requests: make(chan *testTransmission, 16), + responses: make(chan *transmissionResult, 16), + } + + var client TelemetryClient + if len(config) > 0 { + client = NewTelemetryClientFromConfig(config[0]) + } else { + config := NewTelemetryConfiguration("") + config.MaxBatchInterval = ten_seconds // assumed by every test. + client = NewTelemetryClientFromConfig(config) + } + + client.(*telemetryClient).channel.(*InMemoryChannel).transmitter = transmitter + + return client, transmitter +} + +func assertTimeApprox(t *testing.T, x, y time.Time) { + const delta = (time.Duration(100) * time.Millisecond) + if (x.Before(y) && y.Sub(x) > delta) || (y.Before(x) && x.Sub(y) > delta) { + t.Errorf("Time isn't a close match: %v vs %v", x, y) + } +} + +func assertNotClosed(t *testing.T, ch <-chan struct{}) { + select { + case <-ch: + t.Fatal("Close signal was not expected to be received") + default: + } +} + +func waitForClose(t *testing.T, ch <-chan struct{}) bool { + select { + case <-ch: + return true + case <-time.After(time.Duration(100) * time.Second): + t.Fatal("Close signal not received in 100ms") + return false /* not reached */ + } +} + +func TestSimpleSubmit(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + defer client.Channel().Stop() + + client.TrackTrace("~msg~", Information) + tm := currentClock.Now() + transmitter.prepResponse(200) + + slowTick(11) + req := transmitter.waitForRequest(t) + + assertTimeApprox(t, req.timestamp, tm.Add(ten_seconds)) + + if !strings.Contains(string(req.payload), "~msg~") { + t.Errorf("Payload does not contain message") + } +} + +func TestMultipleSubmit(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + defer client.Channel().Stop() + + transmitter.prepResponse(200, 200) + + start := currentClock.Now() + + for i := 0; i < 16; i++ { + client.TrackTrace(fmt.Sprintf("~msg-%x~", i), Information) + slowTick(1) + } + + slowTick(10) + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, start.Add(ten_seconds)) + + for i := 0; i < 10; i++ { + if !strings.Contains(req1.payload, fmt.Sprintf("~msg-%x~", i)) { + t.Errorf("Payload does not contain expected item: %x", i) + } + } + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, start.Add(ten_seconds+ten_seconds)) + + for i := 10; i < 16; i++ { + if !strings.Contains(req2.payload, fmt.Sprintf("~msg-%x~", i)) { + t.Errorf("Payload does not contain expected item: %x", i) + } + } +} + +func TestFlush(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + defer client.Channel().Stop() + + transmitter.prepResponse(200, 200) + + // Empty flush should do nothing + client.Channel().Flush() + + tm := currentClock.Now() + client.TrackTrace("~msg~", Information) + client.Channel().Flush() + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, tm) + if !strings.Contains(req1.payload, "~msg~") { + t.Error("Unexpected payload") + } + + // Next one goes back to normal + client.TrackTrace("~next~", Information) + slowTick(11) + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds)) + if !strings.Contains(req2.payload, "~next~") { + t.Error("Unexpected payload") + } +} + +func TestStop(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + + transmitter.prepResponse(200) + + client.TrackTrace("Not sent", Information) + client.Channel().Stop() + slowTick(20) + transmitter.assertNoRequest(t) +} + +func TestCloseFlush(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + + transmitter.prepResponse(200) + + client.TrackTrace("~flushed~", Information) + client.Channel().Close() + + req := transmitter.waitForRequest(t) + if !strings.Contains(req.payload, "~flushed~") { + t.Error("Unexpected payload") + } +} + +func TestCloseFlushRetry(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + + transmitter.prepResponse(500, 200) + + client.TrackTrace("~flushed~", Information) + tm := currentClock.Now() + ch := client.Channel().Close(time.Minute) + + slowTick(30) + + waitForClose(t, ch) + + req1 := transmitter.waitForRequest(t) + if !strings.Contains(req1.payload, "~flushed~") { + t.Error("Unexpected payload") + } + + assertTimeApprox(t, req1.timestamp, tm) + + req2 := transmitter.waitForRequest(t) + if !strings.Contains(req2.payload, "~flushed~") { + t.Error("Unexpected payload") + } + + assertTimeApprox(t, req2.timestamp, tm.Add(submit_retries[0])) +} + +func TestCloseWithOngoingRetry(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer transmitter.Close() + + transmitter.prepResponse(408, 200, 200) + + // This message should get stuck, retried + client.TrackTrace("~msg-1~", Information) + slowTick(11) + + // Check first one came through + req1 := transmitter.waitForRequest(t) + if !strings.Contains(req1.payload, "~msg-1~") { + t.Error("First message unexpected payload") + } + + // This message will get flushed immediately + client.TrackTrace("~msg-2~", Information) + ch := client.Channel().Close(time.Minute) + + // Let 2 go out, but not the retry for 1 + slowTick(3) + + assertNotClosed(t, ch) + + req2 := transmitter.waitForRequest(t) + if !strings.Contains(req2.payload, "~msg-2~") { + t.Error("Second message unexpected payload") + } + + // Then, let's wait for the first message to go out... + slowTick(20) + + waitForClose(t, ch) + + req3 := transmitter.waitForRequest(t) + if !strings.Contains(req3.payload, "~msg-1~") { + t.Error("Third message unexpected payload") + } +} + +func TestSendOnBufferFull(t *testing.T) { + mockClock() + defer resetClock() + + config := NewTelemetryConfiguration("") + config.MaxBatchSize = 4 + client, transmitter := newTestChannelServer(config) + defer transmitter.Close() + defer client.Channel().Stop() + + transmitter.prepResponse(200, 200) + + for i := 0; i < 5; i++ { + client.TrackTrace(fmt.Sprintf("~msg-%d~", i), Information) + } + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, currentClock.Now()) + + for i := 0; i < 4; i++ { + if !strings.Contains(req1.payload, fmt.Sprintf("~msg-%d~", i)) || len(req1.items) != 4 { + t.Errorf("Payload does not contain expected message") + } + } + + slowTick(5) + transmitter.assertNoRequest(t) + slowTick(5) + + // The last one should have gone out as normal + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, currentClock.Now()) + if !strings.Contains(req2.payload, "~msg-4~") || len(req2.items) != 1 { + t.Errorf("Payload does not contain expected message") + } +} + +func TestRetryOnFailure(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer client.Channel().Stop() + defer transmitter.Close() + + transmitter.prepResponse(500, 200) + + client.TrackTrace("~msg-1~", Information) + client.TrackTrace("~msg-2~", Information) + + tm := currentClock.Now() + slowTick(10) + + req1 := transmitter.waitForRequest(t) + if !strings.Contains(req1.payload, "~msg-1~") || !strings.Contains(req1.payload, "~msg-2~") || len(req1.items) != 2 { + t.Error("Unexpected payload") + } + + assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) + + slowTick(30) + + req2 := transmitter.waitForRequest(t) + if req2.payload != req1.payload || len(req2.items) != 2 { + t.Error("Unexpected payload") + } + + assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds).Add(submit_retries[0])) +} + +func TestPartialRetry(t *testing.T) { + mockClock() + defer resetClock() + client, transmitter := newTestChannelServer() + defer client.Channel().Stop() + defer transmitter.Close() + + client.TrackTrace("~ok-1~", Information) + client.TrackTrace("~retry-1~", Information) + client.TrackTrace("~ok-2~", Information) + client.TrackTrace("~bad-1~", Information) + client.TrackTrace("~retry-2~", Information) + + transmitter.responses <- &transmissionResult{ + statusCode: 206, + response: &backendResponse{ + ItemsAccepted: 2, + ItemsReceived: 5, + Errors: []*itemTransmissionResult{ + &itemTransmissionResult{Index: 1, StatusCode: 500, Message: "Server Error"}, + &itemTransmissionResult{Index: 2, StatusCode: 200, Message: "OK"}, + &itemTransmissionResult{Index: 3, StatusCode: 400, Message: "Bad Request"}, + &itemTransmissionResult{Index: 4, StatusCode: 408, Message: "Plz Retry"}, + }, + }, + } + + transmitter.prepResponse(200) + + tm := currentClock.Now() + slowTick(30) + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) + if len(req1.items) != 5 { + t.Error("Unexpected payload") + } + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds).Add(submit_retries[0])) + if len(req2.items) != 2 { + t.Error("Unexpected payload") + } + + if strings.Contains(req2.payload, "~ok-") || strings.Contains(req2.payload, "~bad-") || !strings.Contains(req2.payload, "~retry-") { + t.Error("Unexpected payload") + } +} + +func TestThrottleDropsMessages(t *testing.T) { + mockClock() + defer resetClock() + config := NewTelemetryConfiguration("") + config.MaxBatchSize = 4 + client, transmitter := newTestChannelServer(config) + defer client.Channel().Stop() + defer transmitter.Close() + + tm := currentClock.Now() + retryAfter := transmitter.prepThrottle(time.Minute) + transmitter.prepResponse(200, 200) + + client.TrackTrace("~throttled~", Information) + slowTick(10) + + for i := 0; i < 20; i++ { + client.TrackTrace(fmt.Sprintf("~msg-%d~", i), Information) + } + + slowTick(60) + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) + if len(req1.items) != 1 || !strings.Contains(req1.payload, "~throttled~") || strings.Contains(req1.payload, "~msg-") { + t.Error("Unexpected payload") + } + + // Humm.. this might break- these two could flip places. But I haven't seen it happen yet. + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, retryAfter) + if len(req2.items) != 1 || !strings.Contains(req2.payload, "~throttled~") || strings.Contains(req2.payload, "~msg-") { + t.Error("Unexpected payload") + } + + req3 := transmitter.waitForRequest(t) + assertTimeApprox(t, req3.timestamp, retryAfter) + if len(req3.items) != 4 || strings.Contains(req3.payload, "~throttled-") || !strings.Contains(req3.payload, "~msg-") { + t.Error("Unexpected payload") + } + + transmitter.assertNoRequest(t) +} + +func TestThrottleCannotFlush(t *testing.T) { + mockClock() + defer resetClock() + config := NewTelemetryConfiguration("") + config.MaxBatchSize = 4 + client, transmitter := newTestChannelServer(config) + defer client.Channel().Stop() + defer transmitter.Close() + + tm := currentClock.Now() + retryAfter := transmitter.prepThrottle(time.Minute) + + transmitter.prepResponse(200, 200) + + client.TrackTrace("~throttled~", Information) + slowTick(10) + + client.TrackTrace("~msg~", Information) + client.Channel().Flush() + + slowTick(60) + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, retryAfter) + + req3 := transmitter.waitForRequest(t) + assertTimeApprox(t, req3.timestamp, retryAfter) + + transmitter.assertNoRequest(t) +} + +func TestThrottleFlushesOnClose(t *testing.T) { + mockClock() + defer resetClock() + config := NewTelemetryConfiguration("") + config.MaxBatchSize = 4 + client, transmitter := newTestChannelServer(config) + defer transmitter.Close() + + tm := currentClock.Now() + retryAfter := transmitter.prepThrottle(time.Minute) + + transmitter.prepResponse(200, 200) + + client.TrackTrace("~throttled~", Information) + slowTick(10) + + client.TrackTrace("~msg~", Information) + ch := client.Channel().Close(30 * time.Second) + + slowTick(60) + + waitForClose(t, ch) + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) + if !strings.Contains(req1.payload, "~throttled~") || len(req1.items) != 1 { + t.Error("Unexpected payload") + } + + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds)) + if !strings.Contains(req2.payload, "~msg~") || len(req2.items) != 1 { + t.Error("Unexpected payload") + } + + req3 := transmitter.waitForRequest(t) + assertTimeApprox(t, req3.timestamp, retryAfter) + if !strings.Contains(req3.payload, "~throttled~") || len(req3.items) != 1 { + t.Error("Unexpected payload") + } + + transmitter.assertNoRequest(t) +} + +func TestThrottleAbandonsMessageOnStop(t *testing.T) { + mockClock() + defer resetClock() + config := NewTelemetryConfiguration("") + config.MaxBatchSize = 4 + client, transmitter := newTestChannelServer(config) + defer transmitter.Close() + + transmitter.prepThrottle(time.Minute) + transmitter.prepResponse(200, 200, 200, 200) + + client.TrackTrace("~throttled~", Information) + slowTick(10) + client.TrackTrace("~dropped~", Information) + slowTick(10) + client.Channel().Stop() + slowTick(45) + + // ~throttled~ will get retried after throttle is done; ~dropped~ should get lost. + for i := 0; i < 2; i++ { + req := transmitter.waitForRequest(t) + if strings.Contains(req.payload, "~dropped~") || len(req.items) != 1 { + t.Fatal("Dropped should have never been sent") + } + } + + transmitter.assertNoRequest(t) +} + +func TestThrottleStacking(t *testing.T) { + mockClock() + defer resetClock() + config := NewTelemetryConfiguration("") + config.MaxBatchSize = 1 + client, transmitter := newTestChannelServer(config) + defer transmitter.Close() + + // It's easy to hit a race in this test. There are two places that check for + // a throttle: one in the channel accept loop, the other in transmitRetry. + // For this test, I want both to hit the one in transmitRetry and then each + // make further attempts in lock-step from there. + + start := currentClock.Now() + client.TrackTrace("~throttle-1~", Information) + client.TrackTrace("~throttle-2~", Information) + + // Per above, give both time to get to transmitRetry, then send out responses + // simultaneously. + slowTick(10) + + transmitter.prepThrottle(20 * time.Second) + second_tm := transmitter.prepThrottle(time.Minute) + + transmitter.prepResponse(200, 200, 200) + + slowTick(65) + + req1 := transmitter.waitForRequest(t) + assertTimeApprox(t, req1.timestamp, start) + req2 := transmitter.waitForRequest(t) + assertTimeApprox(t, req2.timestamp, start) + + req3 := transmitter.waitForRequest(t) + assertTimeApprox(t, req3.timestamp, second_tm) + req4 := transmitter.waitForRequest(t) + assertTimeApprox(t, req4.timestamp, second_tm) + + transmitter.assertNoRequest(t) +} diff --git a/application-insights/appinsights/jsonserializer.go b/application-insights/appinsights/jsonserializer.go new file mode 100644 index 0000000000..4706cd764d --- /dev/null +++ b/application-insights/appinsights/jsonserializer.go @@ -0,0 +1,25 @@ +package appinsights + +import ( + "bytes" + "encoding/json" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +type telemetryBufferItems []*contracts.Envelope + +func (items telemetryBufferItems) serialize() []byte { + var result bytes.Buffer + encoder := json.NewEncoder(&result) + + for _, item := range items { + end := result.Len() + if err := encoder.Encode(item); err != nil { + diagnosticsWriter.Printf("Telemetry item failed to serialize: %s", err.Error()) + result.Truncate(end) + } + } + + return result.Bytes() +} diff --git a/application-insights/appinsights/jsonserializer_test.go b/application-insights/appinsights/jsonserializer_test.go new file mode 100644 index 0000000000..015b61db49 --- /dev/null +++ b/application-insights/appinsights/jsonserializer_test.go @@ -0,0 +1,483 @@ +package appinsights + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "strconv" + "strings" + "testing" + "time" +) + +const test_ikey = "01234567-0000-89ab-cdef-000000000000" + +func TestJsonSerializerEvents(t *testing.T) { + mockClock(time.Unix(1511001321, 0)) + defer resetClock() + + var buffer telemetryBufferItems + + buffer.add( + NewTraceTelemetry("testing", Error), + NewEventTelemetry("an-event"), + NewMetricTelemetry("a-metric", 567), + ) + + req := NewRequestTelemetry("method", "my-url", time.Minute, "204") + req.Name = "req-name" + req.Id = "my-id" + buffer.add(req) + + agg := NewAggregateMetricTelemetry("agg-metric") + agg.AddData([]float64{1, 2, 3}) + buffer.add(agg) + + remdep := NewRemoteDependencyTelemetry("bing-remote-dep", "http", "www.bing.com", false) + remdep.Data = "some-data" + remdep.ResultCode = "arg" + remdep.Duration = 4 * time.Second + remdep.Properties["hi"] = "hello" + buffer.add(remdep) + + avail := NewAvailabilityTelemetry("webtest", 8*time.Second, true) + avail.RunLocation = "jupiter" + avail.Message = "ok." + avail.Measurements["measure"] = 88.0 + avail.Id = "avail-id" + buffer.add(avail) + + view := NewPageViewTelemetry("name", "http://bing.com") + view.Duration = 4 * time.Minute + buffer.add(view) + + j, err := parsePayload(buffer.serialize()) + if err != nil { + t.Errorf("Error parsing payload: %s", err.Error()) + } + + if len(j) != 8 { + t.Fatal("Unexpected event count") + } + + // Trace + j[0].assertPath(t, "iKey", test_ikey) + j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") + j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[0].assertPath(t, "sampleRate", 100.0) + j[0].assertPath(t, "data.baseType", "MessageData") + j[0].assertPath(t, "data.baseData.message", "testing") + j[0].assertPath(t, "data.baseData.severityLevel", 3) + j[0].assertPath(t, "data.baseData.ver", 2) + + // Event + j[1].assertPath(t, "iKey", test_ikey) + j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") + j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[1].assertPath(t, "sampleRate", 100.0) + j[1].assertPath(t, "data.baseType", "EventData") + j[1].assertPath(t, "data.baseData.name", "an-event") + j[1].assertPath(t, "data.baseData.ver", 2) + + // Metric + j[2].assertPath(t, "iKey", test_ikey) + j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") + j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[2].assertPath(t, "sampleRate", 100.0) + j[2].assertPath(t, "data.baseType", "MetricData") + j[2].assertPath(t, "data.baseData.metrics.", 1) + j[2].assertPath(t, "data.baseData.metrics.[0].value", 567) + j[2].assertPath(t, "data.baseData.metrics.[0].count", 1) + j[2].assertPath(t, "data.baseData.metrics.[0].kind", 0) + j[2].assertPath(t, "data.baseData.metrics.[0].name", "a-metric") + j[2].assertPath(t, "data.baseData.ver", 2) + + // Request + j[3].assertPath(t, "iKey", test_ikey) + j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") + j[3].assertPath(t, "time", "2017-11-18T10:34:21Z") // Constructor subtracts duration + j[3].assertPath(t, "sampleRate", 100.0) + j[3].assertPath(t, "data.baseType", "RequestData") + j[3].assertPath(t, "data.baseData.name", "req-name") + j[3].assertPath(t, "data.baseData.duration", "0.00:01:00.0000000") + j[3].assertPath(t, "data.baseData.responseCode", "204") + j[3].assertPath(t, "data.baseData.success", true) + j[3].assertPath(t, "data.baseData.id", "my-id") + j[3].assertPath(t, "data.baseData.url", "my-url") + j[3].assertPath(t, "data.baseData.ver", 2) + + // Aggregate metric + j[4].assertPath(t, "iKey", test_ikey) + j[4].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") + j[4].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[4].assertPath(t, "sampleRate", 100.0) + j[4].assertPath(t, "data.baseType", "MetricData") + j[4].assertPath(t, "data.baseData.metrics.", 1) + j[4].assertPath(t, "data.baseData.metrics.[0].value", 6) + j[4].assertPath(t, "data.baseData.metrics.[0].count", 3) + j[4].assertPath(t, "data.baseData.metrics.[0].kind", 1) + j[4].assertPath(t, "data.baseData.metrics.[0].min", 1) + j[4].assertPath(t, "data.baseData.metrics.[0].max", 3) + j[4].assertPath(t, "data.baseData.metrics.[0].stdDev", 0.8164) + j[4].assertPath(t, "data.baseData.metrics.[0].name", "agg-metric") + j[4].assertPath(t, "data.baseData.ver", 2) + + // Remote dependency + j[5].assertPath(t, "iKey", test_ikey) + j[5].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.RemoteDependency") + j[5].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[5].assertPath(t, "sampleRate", 100.0) + j[5].assertPath(t, "data.baseType", "RemoteDependencyData") + j[5].assertPath(t, "data.baseData.name", "bing-remote-dep") + j[5].assertPath(t, "data.baseData.id", "") + j[5].assertPath(t, "data.baseData.resultCode", "arg") + j[5].assertPath(t, "data.baseData.duration", "0.00:00:04.0000000") + j[5].assertPath(t, "data.baseData.success", false) + j[5].assertPath(t, "data.baseData.data", "some-data") + j[5].assertPath(t, "data.baseData.target", "www.bing.com") + j[5].assertPath(t, "data.baseData.type", "http") + j[5].assertPath(t, "data.baseData.properties.hi", "hello") + j[5].assertPath(t, "data.baseData.ver", 2) + + // Availability + j[6].assertPath(t, "iKey", test_ikey) + j[6].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Availability") + j[6].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[6].assertPath(t, "sampleRate", 100.0) + j[6].assertPath(t, "data.baseType", "AvailabilityData") + j[6].assertPath(t, "data.baseData.name", "webtest") + j[6].assertPath(t, "data.baseData.duration", "0.00:00:08.0000000") + j[6].assertPath(t, "data.baseData.success", true) + j[6].assertPath(t, "data.baseData.runLocation", "jupiter") + j[6].assertPath(t, "data.baseData.message", "ok.") + j[6].assertPath(t, "data.baseData.id", "avail-id") + j[6].assertPath(t, "data.baseData.ver", 2) + j[6].assertPath(t, "data.baseData.measurements.measure", 88) + + // Page view + j[7].assertPath(t, "iKey", test_ikey) + j[7].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.PageView") + j[7].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[7].assertPath(t, "sampleRate", 100.0) + j[7].assertPath(t, "data.baseType", "PageViewData") + j[7].assertPath(t, "data.baseData.name", "name") + j[7].assertPath(t, "data.baseData.url", "http://bing.com") + j[7].assertPath(t, "data.baseData.duration", "0.00:04:00.0000000") + j[7].assertPath(t, "data.baseData.ver", 2) +} + +func TestJsonSerializerNakedEvents(t *testing.T) { + mockClock(time.Unix(1511001321, 0)) + defer resetClock() + + var buffer telemetryBufferItems + + buffer.add( + &TraceTelemetry{ + Message: "Naked telemetry", + SeverityLevel: Warning, + }, + &EventTelemetry{ + Name: "Naked event", + }, + &MetricTelemetry{ + Name: "my-metric", + Value: 456.0, + }, + &AggregateMetricTelemetry{ + Name: "agg-metric", + Value: 50, + Min: 2, + Max: 7, + Count: 9, + StdDev: 3, + }, + &RequestTelemetry{ + Name: "req-name", + Url: "req-url", + Duration: time.Minute, + ResponseCode: "Response", + Success: true, + Source: "localhost", + }, + &RemoteDependencyTelemetry{ + Name: "dep-name", + ResultCode: "ok.", + Duration: time.Hour, + Success: true, + Data: "dep-data", + Type: "dep-type", + Target: "dep-target", + }, + &AvailabilityTelemetry{ + Name: "avail-name", + Duration: 3 * time.Minute, + Success: true, + RunLocation: "run-loc", + Message: "avail-msg", + }, + &PageViewTelemetry{ + Url: "page-view-url", + Duration: 4 * time.Second, + Name: "page-view-name", + }, + ) + + j, err := parsePayload(buffer.serialize()) + if err != nil { + t.Errorf("Error parsing payload: %s", err.Error()) + } + + if len(j) != 8 { + t.Fatal("Unexpected event count") + } + + // Trace + j[0].assertPath(t, "iKey", test_ikey) + j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") + j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[0].assertPath(t, "sampleRate", 100) + j[0].assertPath(t, "data.baseType", "MessageData") + j[0].assertPath(t, "data.baseData.message", "Naked telemetry") + j[0].assertPath(t, "data.baseData.severityLevel", 2) + j[0].assertPath(t, "data.baseData.ver", 2) + + // Event + j[1].assertPath(t, "iKey", test_ikey) + j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") + j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[1].assertPath(t, "sampleRate", 100) + j[1].assertPath(t, "data.baseType", "EventData") + j[1].assertPath(t, "data.baseData.name", "Naked event") + j[1].assertPath(t, "data.baseData.ver", 2) + + // Metric + j[2].assertPath(t, "iKey", test_ikey) + j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") + j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[2].assertPath(t, "sampleRate", 100) + j[2].assertPath(t, "data.baseType", "MetricData") + j[2].assertPath(t, "data.baseData.metrics.", 1) + j[2].assertPath(t, "data.baseData.metrics.[0].value", 456) + j[2].assertPath(t, "data.baseData.metrics.[0].count", 1) + j[2].assertPath(t, "data.baseData.metrics.[0].kind", 0) + j[2].assertPath(t, "data.baseData.metrics.[0].name", "my-metric") + j[2].assertPath(t, "data.baseData.ver", 2) + + // Aggregate metric + j[3].assertPath(t, "iKey", test_ikey) + j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") + j[3].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[3].assertPath(t, "sampleRate", 100.0) + j[3].assertPath(t, "data.baseType", "MetricData") + j[3].assertPath(t, "data.baseData.metrics.", 1) + j[3].assertPath(t, "data.baseData.metrics.[0].value", 50) + j[3].assertPath(t, "data.baseData.metrics.[0].count", 9) + j[3].assertPath(t, "data.baseData.metrics.[0].kind", 1) + j[3].assertPath(t, "data.baseData.metrics.[0].min", 2) + j[3].assertPath(t, "data.baseData.metrics.[0].max", 7) + j[3].assertPath(t, "data.baseData.metrics.[0].stdDev", 3) + j[3].assertPath(t, "data.baseData.metrics.[0].name", "agg-metric") + j[3].assertPath(t, "data.baseData.ver", 2) + + // Request + j[4].assertPath(t, "iKey", test_ikey) + j[4].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") + j[4].assertPath(t, "time", "2017-11-18T10:35:21Z") // Context takes current time since it's not supplied + j[4].assertPath(t, "sampleRate", 100.0) + j[4].assertPath(t, "data.baseType", "RequestData") + j[4].assertPath(t, "data.baseData.name", "req-name") + j[4].assertPath(t, "data.baseData.duration", "0.00:01:00.0000000") + j[4].assertPath(t, "data.baseData.responseCode", "Response") + j[4].assertPath(t, "data.baseData.success", true) + j[4].assertPath(t, "data.baseData.url", "req-url") + j[4].assertPath(t, "data.baseData.source", "localhost") + j[4].assertPath(t, "data.baseData.ver", 2) + + if id, err := j[4].getPath("data.baseData.id"); err != nil { + t.Errorf("Id not present") + } else if len(id.(string)) == 0 { + t.Errorf("Empty request id") + } + + // Remote dependency + j[5].assertPath(t, "iKey", test_ikey) + j[5].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.RemoteDependency") + j[5].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[5].assertPath(t, "sampleRate", 100.0) + j[5].assertPath(t, "data.baseType", "RemoteDependencyData") + j[5].assertPath(t, "data.baseData.name", "dep-name") + j[5].assertPath(t, "data.baseData.id", "") + j[5].assertPath(t, "data.baseData.resultCode", "ok.") + j[5].assertPath(t, "data.baseData.duration", "0.01:00:00.0000000") + j[5].assertPath(t, "data.baseData.success", true) + j[5].assertPath(t, "data.baseData.data", "dep-data") + j[5].assertPath(t, "data.baseData.target", "dep-target") + j[5].assertPath(t, "data.baseData.type", "dep-type") + j[5].assertPath(t, "data.baseData.ver", 2) + + // Availability + j[6].assertPath(t, "iKey", test_ikey) + j[6].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Availability") + j[6].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[6].assertPath(t, "sampleRate", 100.0) + j[6].assertPath(t, "data.baseType", "AvailabilityData") + j[6].assertPath(t, "data.baseData.name", "avail-name") + j[6].assertPath(t, "data.baseData.duration", "0.00:03:00.0000000") + j[6].assertPath(t, "data.baseData.success", true) + j[6].assertPath(t, "data.baseData.runLocation", "run-loc") + j[6].assertPath(t, "data.baseData.message", "avail-msg") + j[6].assertPath(t, "data.baseData.id", "") + j[6].assertPath(t, "data.baseData.ver", 2) + + // Page view + j[7].assertPath(t, "iKey", test_ikey) + j[7].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.PageView") + j[7].assertPath(t, "time", "2017-11-18T10:35:21Z") + j[7].assertPath(t, "sampleRate", 100.0) + j[7].assertPath(t, "data.baseType", "PageViewData") + j[7].assertPath(t, "data.baseData.name", "page-view-name") + j[7].assertPath(t, "data.baseData.url", "page-view-url") + j[7].assertPath(t, "data.baseData.duration", "0.00:00:04.0000000") + j[7].assertPath(t, "data.baseData.ver", 2) +} + +// Test helpers... + +func telemetryBuffer(items ...Telemetry) telemetryBufferItems { + ctx := NewTelemetryContext(test_ikey) + ctx.iKey = test_ikey + + var result telemetryBufferItems + for _, item := range items { + result = append(result, ctx.envelop(item)) + } + + return result +} + +func (buffer *telemetryBufferItems) add(items ...Telemetry) { + *buffer = append(*buffer, telemetryBuffer(items...)...) +} + +type jsonMessage map[string]interface{} +type jsonPayload []jsonMessage + +func parsePayload(payload []byte) (jsonPayload, error) { + // json.Decoder can detect line endings for us but I'd like to explicitly find them. + var result jsonPayload + for _, item := range bytes.Split(payload, []byte("\n")) { + if len(item) == 0 { + continue + } + + decoder := json.NewDecoder(bytes.NewReader(item)) + msg := make(jsonMessage) + if err := decoder.Decode(&msg); err == nil { + result = append(result, msg) + } else { + return result, err + } + } + + return result, nil +} + +func (msg jsonMessage) assertPath(t *testing.T, path string, value interface{}) { + const tolerance = 0.0001 + v, err := msg.getPath(path) + if err != nil { + t.Error(err.Error()) + return + } + + if num, ok := value.(int); ok { + if vnum, ok := v.(float64); ok { + if math.Abs(float64(num)-vnum) > tolerance { + t.Errorf("Data was unexpected at %s. Got %g want %d", path, vnum, num) + } + } else if vnum, ok := v.(int); ok { + if vnum != num { + t.Errorf("Data was unexpected at %s. Got %d want %d", path, vnum, num) + } + } else { + t.Errorf("Expected value at %s to be a number, but was %T", path, v) + } + } else if num, ok := value.(float64); ok { + if vnum, ok := v.(float64); ok { + if math.Abs(num-vnum) > tolerance { + t.Errorf("Data was unexpected at %s. Got %g want %g", path, vnum, num) + } + } else if vnum, ok := v.(int); ok { + if math.Abs(num-float64(vnum)) > tolerance { + t.Errorf("Data was unexpected at %s. Got %d want %g", path, vnum, num) + } + } else { + t.Errorf("Expected value at %s to be a number, but was %T", path, v) + } + } else if str, ok := value.(string); ok { + if vstr, ok := v.(string); ok { + if str != vstr { + t.Errorf("Data was unexpected at %s. Got '%s' want '%s'", path, vstr, str) + } + } else { + t.Errorf("Expected value at %s to be a string, but was %T", path, v) + } + } else if bl, ok := value.(bool); ok { + if vbool, ok := v.(bool); ok { + if bl != vbool { + t.Errorf("Data was unexpected at %s. Got %t want %t", path, vbool, bl) + } + } else { + t.Errorf("Expected value at %s to be a bool, but was %T", path, v) + } + } else { + t.Errorf("Unsupported type: %#v", value) + } +} + +func (msg jsonMessage) getPath(path string) (interface{}, error) { + parts := strings.Split(path, ".") + var obj interface{} = msg + for i, part := range parts { + if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { + // Array + idxstr := part[1 : len(part)-2] + idx, _ := strconv.Atoi(idxstr) + + if ar, ok := obj.([]interface{}); ok { + if idx >= len(ar) { + return nil, fmt.Errorf("Index out of bounds: %s", strings.Join(parts[0:i+1], ".")) + } + + obj = ar[idx] + } else { + return nil, fmt.Errorf("Path %s is not an array", strings.Join(parts[0:i], ".")) + } + } else if part == "" { + if ar, ok := obj.([]interface{}); ok { + return len(ar), nil + } + } else { + // Map + if dict, ok := obj.(jsonMessage); ok { + if val, ok := dict[part]; ok { + obj = val + } else { + return nil, fmt.Errorf("Key %s not found in %s", part, strings.Join(parts[0:i], ".")) + } + } else if dict, ok := obj.(map[string]interface{}); ok { + if val, ok := dict[part]; ok { + obj = val + } else { + return nil, fmt.Errorf("Key %s not found in %s", part, strings.Join(parts[0:i], ".")) + } + } else { + return nil, fmt.Errorf("Path %s is not a map", strings.Join(parts[0:i], ".")) + } + } + } + + return obj, nil +} diff --git a/application-insights/appinsights/package.go b/application-insights/appinsights/package.go new file mode 100644 index 0000000000..8944a51617 --- /dev/null +++ b/application-insights/appinsights/package.go @@ -0,0 +1,8 @@ +// Package appinsights provides an interface to submit telemetry to Application Insights. +// See more at https://azure.microsoft.com/en-us/services/application-insights/ +package appinsights + +const ( + sdkName = "go" + Version = "0.4.4" +) diff --git a/application-insights/appinsights/telemetry.go b/application-insights/appinsights/telemetry.go new file mode 100644 index 0000000000..54b88781e7 --- /dev/null +++ b/application-insights/appinsights/telemetry.go @@ -0,0 +1,652 @@ +package appinsights + +import ( + "fmt" + "math" + "net/url" + "strconv" + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +// Common interface implemented by telemetry data contracts +type TelemetryData interface { + EnvelopeName(string) string + BaseType() string + Sanitize() []string +} + +// Common interface implemented by telemetry items that can be passed to +// TelemetryClient.Track +type Telemetry interface { + // Gets the time when this item was measured + Time() time.Time + + // Sets the timestamp to the specified time. + SetTime(time.Time) + + // Gets context data containing extra, optional tags. Overrides + // values found on client TelemetryContext. + ContextTags() map[string]string + + // Gets the data contract as it will be submitted to the data + // collector. + TelemetryData() TelemetryData + + // Gets custom properties to submit with the telemetry item. + GetProperties() map[string]string + + // Gets custom measurements to submit with the telemetry item. + GetMeasurements() map[string]float64 +} + +// BaseTelemetry is the common base struct for telemetry items. +type BaseTelemetry struct { + // The time this when this item was measured + Timestamp time.Time + + // Custom properties + Properties map[string]string + + // Telemetry Context containing extra, optional tags. + Tags contracts.ContextTags +} + +// BaseTelemetryMeasurements provides the Measurements field for telemetry +// items that support it. +type BaseTelemetryMeasurements struct { + // Custom measurements + Measurements map[string]float64 +} + +// BaseTelemetryNoMeasurements provides no Measurements field for telemetry +// items that omit it. +type BaseTelemetryNoMeasurements struct { +} + +// Time returns the timestamp when this was measured. +func (item *BaseTelemetry) Time() time.Time { + return item.Timestamp +} + +// SetTime sets the timestamp to the specified time. +func (item *BaseTelemetry) SetTime(t time.Time) { + item.Timestamp = t +} + +// Gets context data containing extra, optional tags. Overrides values +// found on client TelemetryContext. +func (item *BaseTelemetry) ContextTags() map[string]string { + return item.Tags +} + +// Gets custom properties to submit with the telemetry item. +func (item *BaseTelemetry) GetProperties() map[string]string { + return item.Properties +} + +// Gets custom measurements to submit with the telemetry item. +func (item *BaseTelemetryMeasurements) GetMeasurements() map[string]float64 { + return item.Measurements +} + +// GetMeasurements returns nil for telemetry items that do not support measurements. +func (item *BaseTelemetryNoMeasurements) GetMeasurements() map[string]float64 { + return nil +} + +// Trace telemetry items represent printf-like trace statements that can be +// text searched. +type TraceTelemetry struct { + BaseTelemetry + BaseTelemetryNoMeasurements + + // Trace message + Message string + + // Severity level + SeverityLevel contracts.SeverityLevel +} + +// Creates a trace telemetry item with the specified message and severity +// level. +func NewTraceTelemetry(message string, severityLevel contracts.SeverityLevel) *TraceTelemetry { + return &TraceTelemetry{ + Message: message, + SeverityLevel: severityLevel, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + } +} + +func (trace *TraceTelemetry) TelemetryData() TelemetryData { + data := contracts.NewMessageData() + data.Message = trace.Message + data.Properties = trace.Properties + data.SeverityLevel = trace.SeverityLevel + + return data +} + +// Event telemetry items represent structured event records. +type EventTelemetry struct { + BaseTelemetry + BaseTelemetryMeasurements + + // Event name + Name string +} + +// Creates an event telemetry item with the specified name. +func NewEventTelemetry(name string) *EventTelemetry { + return &EventTelemetry{ + Name: name, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + BaseTelemetryMeasurements: BaseTelemetryMeasurements{ + Measurements: make(map[string]float64), + }, + } +} + +func (event *EventTelemetry) TelemetryData() TelemetryData { + data := contracts.NewEventData() + data.Name = event.Name + data.Properties = event.Properties + data.Measurements = event.Measurements + + return data +} + +// Metric telemetry items each represent a single data point. +type MetricTelemetry struct { + BaseTelemetry + BaseTelemetryNoMeasurements + + // Metric name + Name string + + // Sampled value + Value float64 +} + +// Creates a metric telemetry sample with the specified name and value. +func NewMetricTelemetry(name string, value float64) *MetricTelemetry { + return &MetricTelemetry{ + Name: name, + Value: value, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + } +} + +func (metric *MetricTelemetry) TelemetryData() TelemetryData { + dataPoint := contracts.NewDataPoint() + dataPoint.Name = metric.Name + dataPoint.Value = metric.Value + dataPoint.Count = 1 + dataPoint.Kind = contracts.Measurement + + data := contracts.NewMetricData() + data.Metrics = []*contracts.DataPoint{dataPoint} + data.Properties = metric.Properties + + return data +} + +// Aggregated metric telemetry items represent an aggregation of data points +// over time. These values can be calculated by the caller or with the AddData +// function. +type AggregateMetricTelemetry struct { + BaseTelemetry + BaseTelemetryNoMeasurements + + // Metric name + Name string + + // Sum of individual measurements + Value float64 + + // Minimum value of the aggregated metric + Min float64 + + // Maximum value of the aggregated metric + Max float64 + + // Count of measurements in the sample + Count int + + // Standard deviation of the aggregated metric + StdDev float64 + + // Variance of the aggregated metric. As an invariant, + // either this or the StdDev should be zero at any given time. + // If both are non-zero then StdDev takes precedence. + Variance float64 +} + +// Creates a new aggregated metric telemetry item with the specified name. +// Values should be set on the object returned before submission. +func NewAggregateMetricTelemetry(name string) *AggregateMetricTelemetry { + return &AggregateMetricTelemetry{ + Name: name, + Count: 0, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + } +} + +// Adds data points to the aggregate totals included in this telemetry item. +// This can be used for all the data at once or incrementally. Calculates +// Min, Max, Sum, Count, and StdDev (by way of Variance). +func (agg *AggregateMetricTelemetry) AddData(values []float64) { + if agg.StdDev != 0.0 { + // If StdDev is non-zero, then square it to produce + // the variance, which is better for incremental calculations, + // and then zero it out. + agg.Variance = agg.StdDev * agg.StdDev + agg.StdDev = 0.0 + } + + vsum := agg.addData(values, agg.Variance*float64(agg.Count)) + if agg.Count > 0 { + agg.Variance = vsum / float64(agg.Count) + } +} + +// Adds sampled data points to the aggregate totals included in this telemetry item. +// This can be used for all the data at once or incrementally. Differs from AddData +// in how it calculates standard deviation, and should not be used interchangeably +// with AddData. +func (agg *AggregateMetricTelemetry) AddSampledData(values []float64) { + if agg.StdDev != 0.0 { + // If StdDev is non-zero, then square it to produce + // the variance, which is better for incremental calculations, + // and then zero it out. + agg.Variance = agg.StdDev * agg.StdDev + agg.StdDev = 0.0 + } + + vsum := agg.addData(values, agg.Variance*float64(agg.Count-1)) + if agg.Count > 1 { + // Sampled values should divide by n-1 + agg.Variance = vsum / float64(agg.Count-1) + } +} + +func (agg *AggregateMetricTelemetry) addData(values []float64, vsum float64) float64 { + if len(values) == 0 { + return vsum + } + + // Running tally of the mean is important for incremental variance computation. + var mean float64 + + if agg.Count == 0 { + agg.Min = values[0] + agg.Max = values[0] + } else { + mean = agg.Value / float64(agg.Count) + } + + for _, x := range values { + // Update Min, Max, Count, and Value + agg.Count++ + agg.Value += x + + if x < agg.Min { + agg.Min = x + } + + if x > agg.Max { + agg.Max = x + } + + // Welford's algorithm to compute variance. The divide occurs in the caller. + newMean := agg.Value / float64(agg.Count) + vsum += (x - mean) * (x - newMean) + mean = newMean + } + + return vsum +} + +func (agg *AggregateMetricTelemetry) TelemetryData() TelemetryData { + dataPoint := contracts.NewDataPoint() + dataPoint.Name = agg.Name + dataPoint.Value = agg.Value + dataPoint.Kind = contracts.Aggregation + dataPoint.Min = agg.Min + dataPoint.Max = agg.Max + dataPoint.Count = agg.Count + + if agg.StdDev != 0.0 { + dataPoint.StdDev = agg.StdDev + } else if agg.Variance > 0.0 { + dataPoint.StdDev = math.Sqrt(agg.Variance) + } + + data := contracts.NewMetricData() + data.Metrics = []*contracts.DataPoint{dataPoint} + data.Properties = agg.Properties + + return data +} + +// Request telemetry items represents completion of an external request to the +// application and contains a summary of that request execution and results. +type RequestTelemetry struct { + BaseTelemetry + BaseTelemetryMeasurements + + // Identifier of a request call instance. Used for correlation between request + // and other telemetry items. + Id string + + // Request name. For HTTP requests it represents the HTTP method and URL path template. + Name string + + // URL of the request with all query string parameters. + Url string + + // Duration to serve the request. + Duration time.Duration + + // Results of a request execution. HTTP status code for HTTP requests. + ResponseCode string + + // Indication of successful or unsuccessful call. + Success bool + + // Source of the request. Examplese are the instrumentation key of the caller + // or the ip address of the caller. + Source string +} + +// Creates a new request telemetry item for HTTP requests. The success value will be +// computed from responseCode, and the timestamp will be set to the current time minus +// the duration. +func NewRequestTelemetry(method, uri string, duration time.Duration, responseCode string) *RequestTelemetry { + success := true + code, err := strconv.Atoi(responseCode) + if err == nil { + success = code < 400 || code == 401 + } + + nameUri := uri + + // Sanitize URL for the request name + if parsedUrl, err := url.Parse(uri); err == nil { + // Remove the query + parsedUrl.RawQuery = "" + parsedUrl.ForceQuery = false + + // Remove the fragment + parsedUrl.Fragment = "" + + // Remove the user info, if any. + parsedUrl.User = nil + + // Write back to name + nameUri = parsedUrl.String() + } + + return &RequestTelemetry{ + Name: fmt.Sprintf("%s %s", method, nameUri), + Url: uri, + Id: newUUID().String(), + Duration: duration, + ResponseCode: responseCode, + Success: success, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now().Add(-duration), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + BaseTelemetryMeasurements: BaseTelemetryMeasurements{ + Measurements: make(map[string]float64), + }, + } +} + +// Sets the timestamp and duration of this telemetry item based on the provided +// start and end times. +func (request *RequestTelemetry) MarkTime(startTime, endTime time.Time) { + request.Timestamp = startTime + request.Duration = endTime.Sub(startTime) +} + +func (request *RequestTelemetry) TelemetryData() TelemetryData { + data := contracts.NewRequestData() + data.Name = request.Name + data.Duration = formatDuration(request.Duration) + data.ResponseCode = request.ResponseCode + data.Success = request.Success + data.Url = request.Url + data.Source = request.Source + + if request.Id == "" { + data.Id = newUUID().String() + } else { + data.Id = request.Id + } + + data.Properties = request.Properties + data.Measurements = request.Measurements + return data +} + +// Remote dependency telemetry items represent interactions of the monitored +// component with a remote component/service like SQL or an HTTP endpoint. +type RemoteDependencyTelemetry struct { + BaseTelemetry + BaseTelemetryMeasurements + + // Name of the command that initiated this dependency call. Low cardinality + // value. Examples are stored procedure name and URL path template. + Name string + + // Identifier of a dependency call instance. Used for correlation with the + // request telemetry item corresponding to this dependency call. + Id string + + // Result code of a dependency call. Examples are SQL error code and HTTP + // status code. + ResultCode string + + // Duration of the remote call. + Duration time.Duration + + // Indication of successful or unsuccessful call. + Success bool + + // Command initiated by this dependency call. Examples are SQL statement and + // HTTP URL's with all the query parameters. + Data string + + // Dependency type name. Very low cardinality. Examples are SQL, Azure table, + // and HTTP. + Type string + + // Target site of a dependency call. Examples are server name, host address. + Target string +} + +// Builds a new Remote Dependency telemetry item, with the specified name, +// dependency type, target site, and success status. +func NewRemoteDependencyTelemetry(name, dependencyType, target string, success bool) *RemoteDependencyTelemetry { + return &RemoteDependencyTelemetry{ + Name: name, + Type: dependencyType, + Target: target, + Success: success, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + BaseTelemetryMeasurements: BaseTelemetryMeasurements{ + Measurements: make(map[string]float64), + }, + } +} + +// Sets the timestamp and duration of this telemetry item based on the provided +// start and end times. +func (telem *RemoteDependencyTelemetry) MarkTime(startTime, endTime time.Time) { + telem.Timestamp = startTime + telem.Duration = endTime.Sub(startTime) +} + +func (telem *RemoteDependencyTelemetry) TelemetryData() TelemetryData { + data := contracts.NewRemoteDependencyData() + data.Name = telem.Name + data.Id = telem.Id + data.ResultCode = telem.ResultCode + data.Duration = formatDuration(telem.Duration) + data.Success = telem.Success + data.Data = telem.Data + data.Target = telem.Target + data.Properties = telem.Properties + data.Measurements = telem.Measurements + data.Type = telem.Type + + return data +} + +// Avaibility telemetry items represent the result of executing an availability +// test. +type AvailabilityTelemetry struct { + BaseTelemetry + BaseTelemetryMeasurements + + // Identifier of a test run. Used to correlate steps of test run and + // telemetry generated by the service. + Id string + + // Name of the test that this result represents. + Name string + + // Duration of the test run. + Duration time.Duration + + // Success flag. + Success bool + + // Name of the location where the test was run. + RunLocation string + + // Diagnostic message for the result. + Message string +} + +// Creates a new availability telemetry item with the specified test name, +// duration and success code. +func NewAvailabilityTelemetry(name string, duration time.Duration, success bool) *AvailabilityTelemetry { + return &AvailabilityTelemetry{ + Name: name, + Duration: duration, + Success: success, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + BaseTelemetryMeasurements: BaseTelemetryMeasurements{ + Measurements: make(map[string]float64), + }, + } +} + +// Sets the timestamp and duration of this telemetry item based on the provided +// start and end times. +func (telem *AvailabilityTelemetry) MarkTime(startTime, endTime time.Time) { + telem.Timestamp = startTime + telem.Duration = endTime.Sub(startTime) +} + +func (telem *AvailabilityTelemetry) TelemetryData() TelemetryData { + data := contracts.NewAvailabilityData() + data.Name = telem.Name + data.Duration = formatDuration(telem.Duration) + data.Success = telem.Success + data.RunLocation = telem.RunLocation + data.Message = telem.Message + data.Properties = telem.Properties + data.Id = telem.Id + data.Measurements = telem.Measurements + + return data +} + +// Page view telemetry items represent generic actions on a page like a button +// click. +type PageViewTelemetry struct { + BaseTelemetry + BaseTelemetryMeasurements + + // Request URL with all query string parameters + Url string + + // Request duration. + Duration time.Duration + + // Event name. + Name string +} + +// Creates a new page view telemetry item with the specified name and url. +func NewPageViewTelemetry(name, url string) *PageViewTelemetry { + return &PageViewTelemetry{ + Name: name, + Url: url, + BaseTelemetry: BaseTelemetry{ + Timestamp: currentClock.Now(), + Tags: make(contracts.ContextTags), + Properties: make(map[string]string), + }, + BaseTelemetryMeasurements: BaseTelemetryMeasurements{ + Measurements: make(map[string]float64), + }, + } +} + +// Sets the timestamp and duration of this telemetry item based on the provided +// start and end times. +func (telem *PageViewTelemetry) MarkTime(startTime, endTime time.Time) { + telem.Timestamp = startTime + telem.Duration = endTime.Sub(startTime) +} + +func (telem *PageViewTelemetry) TelemetryData() TelemetryData { + data := contracts.NewPageViewData() + data.Url = telem.Url + data.Duration = formatDuration(telem.Duration) + data.Name = telem.Name + data.Properties = telem.Properties + data.Measurements = telem.Measurements + return data +} + +func formatDuration(d time.Duration) string { + ticks := int64(d/(time.Nanosecond*100)) % 10000000 + seconds := int64(d/time.Second) % 60 + minutes := int64(d/time.Minute) % 60 + hours := int64(d/time.Hour) % 24 + days := int64(d / (time.Hour * 24)) + + return fmt.Sprintf("%d.%02d:%02d:%02d.%07d", days, hours, minutes, seconds, ticks) +} diff --git a/application-insights/appinsights/telemetry_test.go b/application-insights/appinsights/telemetry_test.go new file mode 100644 index 0000000000..ba66508180 --- /dev/null +++ b/application-insights/appinsights/telemetry_test.go @@ -0,0 +1,368 @@ +package appinsights + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +const float_precision = 1e-4 + +func checkDataContract(t *testing.T, property string, actual, expected interface{}) { + if x, ok := actual.(float64); ok { + if y, ok := expected.(float64); ok { + if math.Abs(x-y) > float_precision { + t.Errorf("Float property %s mismatched; got %f, want %f.\n", property, actual, expected) + } + + return + } + } + + if actual != expected { + t.Errorf("Property %s mismatched; got %v, want %v.\n", property, actual, expected) + } +} + +func checkNotNullOrEmpty(t *testing.T, property string, actual interface{}) { + if actual == nil { + t.Errorf("Property %s was expected not to be null.\n", property) + } else if str, ok := actual.(string); ok && str == "" { + t.Errorf("Property %s was expected not to be an empty string.\n", property) + } +} + +func TestTraceTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewTraceTelemetry("~my message~", Error) + telem.Properties["prop1"] = "value1" + telem.Properties["prop2"] = "value2" + d := telem.TelemetryData().(*contracts.MessageData) + + checkDataContract(t, "Message", d.Message, "~my message~") + checkDataContract(t, "SeverityLevel", d.SeverityLevel, Error) + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") + checkDataContract(t, "Properties[prop2]", d.Properties["prop2"], "value2") + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + telem2 := &TraceTelemetry{ + Message: "~my-2nd-message~", + SeverityLevel: Critical, + } + d2 := telem2.TelemetryData().(*contracts.MessageData) + + checkDataContract(t, "Message", d2.Message, "~my-2nd-message~") + checkDataContract(t, "SeverityLevel", d2.SeverityLevel, Critical) + + var telemInterface Telemetry + if telemInterface = telem; telemInterface.GetMeasurements() != nil { + t.Errorf("Trace.(Telemetry).GetMeasurements should return nil") + } +} + +func TestEventTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewEventTelemetry("~my event~") + telem.Properties["prop1"] = "value1" + telem.Properties["prop2"] = "value2" + telem.Measurements["measure1"] = 1234.0 + telem.Measurements["measure2"] = 5678.0 + d := telem.TelemetryData().(*contracts.EventData) + + checkDataContract(t, "Name", d.Name, "~my event~") + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") + checkDataContract(t, "Properties[prop2]", d.Properties["prop2"], "value2") + checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 1234.0) + checkDataContract(t, "Measurements[measure2]", d.Measurements["measure2"], 5678.0) + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + telem2 := &EventTelemetry{ + Name: "~my-event~", + } + d2 := telem2.TelemetryData().(*contracts.EventData) + + checkDataContract(t, "Name", d2.Name, "~my-event~") +} + +func TestMetricTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewMetricTelemetry("~my metric~", 1234.0) + telem.Properties["prop1"] = "value!" + d := telem.TelemetryData().(*contracts.MetricData) + + checkDataContract(t, "len(Metrics)", len(d.Metrics), 1) + dp := d.Metrics[0] + checkDataContract(t, "DataPoint.Name", dp.Name, "~my metric~") + checkDataContract(t, "DataPoint.Value", dp.Value, 1234.0) + checkDataContract(t, "DataPoint.Kind", dp.Kind, Measurement) + checkDataContract(t, "DataPoint.Count", dp.Count, 1) + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value!") + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + telem2 := &MetricTelemetry{ + Name: "~my metric~", + Value: 5678.0, + } + d2 := telem2.TelemetryData().(*contracts.MetricData) + + checkDataContract(t, "len(Metrics)", len(d2.Metrics), 1) + dp2 := d2.Metrics[0] + checkDataContract(t, "DataPoint.Name", dp2.Name, "~my metric~") + checkDataContract(t, "DataPoint.Value", dp2.Value, 5678.0) + checkDataContract(t, "DataPoint.Kind", dp2.Kind, Measurement) + checkDataContract(t, "DataPoint.Count", dp2.Count, 1) + + var telemInterface Telemetry + if telemInterface = telem; telemInterface.GetMeasurements() != nil { + t.Errorf("Metric.(Telemetry).GetMeasurements should return nil") + } +} + +type statsTest struct { + data []float64 + stdDev float64 + sampledStdDev float64 + min float64 + max float64 +} + +func TestAggregateMetricTelemetry(t *testing.T) { + statsTests := []statsTest{ + statsTest{[]float64{}, 0.0, 0.0, 0.0, 0.0}, + statsTest{[]float64{0.0}, 0.0, 0.0, 0.0, 0.0}, + statsTest{[]float64{50.0}, 0.0, 0.0, 50.0, 50.0}, + statsTest{[]float64{50.0, 50.0}, 0.0, 0.0, 50.0, 50.0}, + statsTest{[]float64{50.0, 60.0}, 5.0, 7.071, 50.0, 60.0}, + statsTest{[]float64{9.0, 10.0, 11.0, 7.0, 13.0}, 2.0, 2.236, 7.0, 13.0}, + // TODO: More tests. + } + + for _, tst := range statsTests { + t1 := NewAggregateMetricTelemetry("foo") + t2 := NewAggregateMetricTelemetry("foo") + t1.AddData(tst.data) + t2.AddSampledData(tst.data) + + checkDataPoint(t, t1, tst, false) + checkDataPoint(t, t2, tst, true) + } + + // Do the same as above, but add data points one at a time. + for _, tst := range statsTests { + t1 := NewAggregateMetricTelemetry("foo") + t2 := NewAggregateMetricTelemetry("foo") + + for _, x := range tst.data { + t1.AddData([]float64{x}) + t2.AddSampledData([]float64{x}) + } + + checkDataPoint(t, t1, tst, false) + checkDataPoint(t, t2, tst, true) + } +} + +func checkDataPoint(t *testing.T, telem *AggregateMetricTelemetry, tst statsTest, sampled bool) { + d := telem.TelemetryData().(*contracts.MetricData) + checkDataContract(t, "len(Metrics)", len(d.Metrics), 1) + dp := d.Metrics[0] + + var sum float64 + for _, x := range tst.data { + sum += x + } + + checkDataContract(t, "DataPoint.Count", dp.Count, len(tst.data)) + checkDataContract(t, "DataPoint.Min", dp.Min, tst.min) + checkDataContract(t, "DataPoint.Max", dp.Max, tst.max) + checkDataContract(t, "DataPoint.Value", dp.Value, sum) + + if sampled { + checkDataContract(t, "DataPoint.StdDev (sample)", dp.StdDev, tst.sampledStdDev) + } else { + checkDataContract(t, "DataPoint.StdDev (population)", dp.StdDev, tst.stdDev) + } +} + +func TestRequestTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewRequestTelemetry("POST", "http://testurl.org/?query=value", time.Minute, "200") + telem.Source = "127.0.0.1" + telem.Properties["prop1"] = "value1" + telem.Measurements["measure1"] = 999.0 + d := telem.TelemetryData().(*contracts.RequestData) + + checkNotNullOrEmpty(t, "Id", d.Id) + checkDataContract(t, "Name", d.Name, "POST http://testurl.org/") + checkDataContract(t, "Url", d.Url, "http://testurl.org/?query=value") + checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") + checkDataContract(t, "Success", d.Success, true) + checkDataContract(t, "Source", d.Source, "127.0.0.1") + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") + checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now().Add(-time.Minute)) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + startTime := currentClock.Now().Add(-time.Hour) + endTime := startTime.Add(5 * time.Minute) + telem.MarkTime(startTime, endTime) + d = telem.TelemetryData().(*contracts.RequestData) + checkDataContract(t, "Timestamp", telem.Time(), startTime) + checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") +} + +func TestRequestTelemetrySuccess(t *testing.T) { + // Some of these are due to default-success + successCodes := []string{"200", "204", "301", "302", "401", "foo", "", "55555555555555555555555555555555555555555555555555"} + failureCodes := []string{"400", "404", "500", "430"} + + for _, code := range successCodes { + telem := NewRequestTelemetry("GET", "https://something", time.Second, code) + d := telem.TelemetryData().(*contracts.RequestData) + checkDataContract(t, fmt.Sprintf("Success [%s]", code), d.Success, true) + } + + for _, code := range failureCodes { + telem := NewRequestTelemetry("GET", "https://something", time.Second, code) + d := telem.TelemetryData().(*contracts.RequestData) + checkDataContract(t, fmt.Sprintf("Success [%s]", code), d.Success, false) + } +} + +func TestRemoteDependencyTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewRemoteDependencyTelemetry("SQL-GET", "SQL", "myhost.name", true) + telem.Data = "" + telem.ResultCode = "OK" + telem.Duration = time.Minute + telem.Properties["prop1"] = "value1" + telem.Measurements["measure1"] = 999.0 + d := telem.TelemetryData().(*contracts.RemoteDependencyData) + + checkDataContract(t, "Id", d.Id, "") // no default + checkDataContract(t, "Data", d.Data, "") + checkDataContract(t, "Type", d.Type, "SQL") + checkDataContract(t, "Target", d.Target, "myhost.name") + checkDataContract(t, "ResultCode", d.ResultCode, "OK") + checkDataContract(t, "Name", d.Name, "SQL-GET") + checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") + checkDataContract(t, "Success", d.Success, true) + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") + checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + telem.Id = "" + telem.Success = false + d = telem.TelemetryData().(*contracts.RemoteDependencyData) + checkDataContract(t, "Id", d.Id, "") + checkDataContract(t, "Success", d.Success, false) + + startTime := currentClock.Now().Add(-time.Hour) + endTime := startTime.Add(5 * time.Minute) + telem.MarkTime(startTime, endTime) + d = telem.TelemetryData().(*contracts.RemoteDependencyData) + checkDataContract(t, "Timestamp", telem.Time(), startTime) + checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") +} + +func TestAvailabilityTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewAvailabilityTelemetry("Frontdoor", time.Minute, true) + telem.RunLocation = "The moon" + telem.Message = "OK" + telem.Properties["prop1"] = "value1" + telem.Measurements["measure1"] = 999.0 + d := telem.TelemetryData().(*contracts.AvailabilityData) + + checkDataContract(t, "Id", d.Id, "") + checkDataContract(t, "Name", d.Name, "Frontdoor") + checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") + checkDataContract(t, "RunLocation", d.RunLocation, "The moon") + checkDataContract(t, "Message", d.Message, "OK") + checkDataContract(t, "Success", d.Success, true) + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") + checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + telem.Id = "" + telem.Success = false + d = telem.TelemetryData().(*contracts.AvailabilityData) + checkDataContract(t, "Id", d.Id, "") + checkDataContract(t, "Success", d.Success, false) + + startTime := currentClock.Now().Add(-time.Hour) + endTime := startTime.Add(5 * time.Minute) + telem.MarkTime(startTime, endTime) + d = telem.TelemetryData().(*contracts.AvailabilityData) + checkDataContract(t, "Timestamp", telem.Time(), startTime) + checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") +} + +func TestPageViewTelemetry(t *testing.T) { + mockClock() + defer resetClock() + + telem := NewPageViewTelemetry("Home page", "http://testuri.org/") + telem.Duration = time.Minute + telem.Properties["prop1"] = "value1" + telem.Measurements["measure1"] = 999.0 + d := telem.TelemetryData().(*contracts.PageViewData) + + checkDataContract(t, "Name", d.Name, "Home page") + checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") + checkDataContract(t, "Url", d.Url, "http://testuri.org/") + checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") + checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) + checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) + checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) + + startTime := currentClock.Now().Add(-time.Hour) + endTime := startTime.Add(5 * time.Minute) + telem.MarkTime(startTime, endTime) + d = telem.TelemetryData().(*contracts.PageViewData) + checkDataContract(t, "Timestamp", telem.Time(), startTime) + checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") +} + +type durationTest struct { + duration time.Duration + expected string +} + +func TestFormatDuration(t *testing.T) { + durationTests := []durationTest{ + durationTest{time.Hour, "0.01:00:00.0000000"}, + durationTest{time.Minute, "0.00:01:00.0000000"}, + durationTest{time.Second, "0.00:00:01.0000000"}, + durationTest{time.Millisecond, "0.00:00:00.0010000"}, + durationTest{100 * time.Nanosecond, "0.00:00:00.0000001"}, + durationTest{(31 * time.Hour) + (25 * time.Minute) + (30 * time.Second) + time.Millisecond, "1.07:25:30.0010000"}, + } + + for _, tst := range durationTests { + actual := formatDuration(tst.duration) + if tst.expected != actual { + t.Errorf("Mismatch. Got %s, want %s (duration %s)\n", actual, tst.expected, tst.duration.String()) + } + } +} diff --git a/application-insights/appinsights/telemetrychannel.go b/application-insights/appinsights/telemetrychannel.go new file mode 100644 index 0000000000..189b3cab82 --- /dev/null +++ b/application-insights/appinsights/telemetrychannel.go @@ -0,0 +1,51 @@ +package appinsights + +import ( + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +// Implementations of TelemetryChannel are responsible for queueing and +// periodically submitting telemetry items. +type TelemetryChannel interface { + // The address of the endpoint to which telemetry is sent + EndpointAddress() string + + // Queues a single telemetry item + Send(*contracts.Envelope) + + // Forces the current queue to be sent + Flush() + + // Tears down the submission goroutines, closes internal channels. + // Any telemetry waiting to be sent is discarded. Further calls to + // Send() have undefined behavior. This is a more abrupt version of + // Close(). + Stop() + + // Returns true if this channel has been throttled by the data + // collector. + IsThrottled() bool + + // Flushes and tears down the submission goroutine and closes + // internal channels. Returns a channel that is closed when all + // pending telemetry items have been submitted and it is safe to + // shut down without losing telemetry. + // + // If retryTimeout is specified and non-zero, then failed + // submissions will be retried until one succeeds or the timeout + // expires, whichever occurs first. A retryTimeout of zero + // indicates that failed submissions will be retried as usual. An + // omitted retryTimeout indicates that submissions should not be + // retried if they fail. + // + // Note that the returned channel may not be closed before + // retryTimeout even if it is specified. This is because + // retryTimeout only applies to the latest telemetry buffer. This + // may be typical for applications that submit a large amount of + // telemetry or are prone to being throttled. When exiting, you + // should select on the result channel and your own timer to avoid + // long delays. + Close(retryTimeout ...time.Duration) <-chan struct{} +} diff --git a/application-insights/appinsights/telemetrycontext.go b/application-insights/appinsights/telemetrycontext.go new file mode 100644 index 0000000000..f54e36d146 --- /dev/null +++ b/application-insights/appinsights/telemetrycontext.go @@ -0,0 +1,104 @@ +package appinsights + +import ( + "strings" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +// Encapsulates contextual data common to all telemetry submitted through a +// TelemetryClient instance such as including instrumentation key, tags, and +// common properties. +type TelemetryContext struct { + // Instrumentation key + iKey string + + // Stripped-down instrumentation key used in envelope name + nameIKey string + + // Collection of tag data to attach to the telemetry item. + Tags contracts.ContextTags + + // Common properties to add to each telemetry item. This only has + // an effect from the TelemetryClient's context instance. This will + // be nil on telemetry items. + CommonProperties map[string]string +} + +// Creates a new, empty TelemetryContext +func NewTelemetryContext(ikey string) *TelemetryContext { + return &TelemetryContext{ + iKey: ikey, + nameIKey: strings.Replace(ikey, "-", "", -1), + Tags: make(contracts.ContextTags), + CommonProperties: make(map[string]string), + } +} + +// Gets the instrumentation key associated with this TelemetryContext. This +// will be an empty string on telemetry items' context instances. +func (context *TelemetryContext) InstrumentationKey() string { + return context.iKey +} + +// Wraps a telemetry item in an envelope with the information found in this +// context. +func (context *TelemetryContext) envelop(item Telemetry) *contracts.Envelope { + // Apply common properties + if props := item.GetProperties(); props != nil && context.CommonProperties != nil { + for k, v := range context.CommonProperties { + if _, ok := props[k]; !ok { + props[k] = v + } + } + } + + tdata := item.TelemetryData() + data := contracts.NewData() + data.BaseType = tdata.BaseType() + data.BaseData = tdata + + envelope := contracts.NewEnvelope() + envelope.Name = tdata.EnvelopeName(context.nameIKey) + envelope.Data = data + envelope.IKey = context.iKey + + timestamp := item.Time() + if timestamp.IsZero() { + timestamp = currentClock.Now() + } + + envelope.Time = timestamp.UTC().Format("2006-01-02T15:04:05.999999Z") + + if contextTags := item.ContextTags(); contextTags != nil { + envelope.Tags = contextTags + + // Copy in default tag values. + for tagkey, tagval := range context.Tags { + if _, ok := contextTags[tagkey]; !ok { + contextTags[tagkey] = tagval + } + } + } else { + // Create new tags object + envelope.Tags = make(map[string]string) + for k, v := range context.Tags { + envelope.Tags[k] = v + } + } + + // Create operation ID if it does not exist + if _, ok := envelope.Tags[contracts.OperationId]; !ok { + envelope.Tags[contracts.OperationId] = newUUID().String() + } + + // Sanitize. + for _, warn := range tdata.Sanitize() { + diagnosticsWriter.Printf("Telemetry data warning: %s", warn) + } + for _, warn := range contracts.SanitizeTags(envelope.Tags) { + diagnosticsWriter.Printf("Telemetry tag warning: %s", warn) + } + + return envelope +} diff --git a/application-insights/appinsights/telemetrycontext_test.go b/application-insights/appinsights/telemetrycontext_test.go new file mode 100644 index 0000000000..ada1a2e2d3 --- /dev/null +++ b/application-insights/appinsights/telemetrycontext_test.go @@ -0,0 +1,145 @@ +package appinsights + +import ( + "strings" + "testing" + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +func TestDefaultTags(t *testing.T) { + context := NewTelemetryContext(test_ikey) + context.Tags["test"] = "OK" + context.Tags["no-write"] = "Fail" + + telem := NewTraceTelemetry("Hello world.", Verbose) + telem.Tags["no-write"] = "OK" + + envelope := context.envelop(telem) + + if envelope.Tags["test"] != "OK" { + t.Error("Default client tags did not propagate to telemetry") + } + + if envelope.Tags["no-write"] != "OK" { + t.Error("Default client tag overwrote telemetry item tag") + } +} + +func TestCommonProperties(t *testing.T) { + context := NewTelemetryContext(test_ikey) + context.CommonProperties = map[string]string{ + "test": "OK", + "no-write": "Fail", + } + + telem := NewTraceTelemetry("Hello world.", Verbose) + telem.Properties["no-write"] = "OK" + + envelope := context.envelop(telem) + data := envelope.Data.(*contracts.Data).BaseData.(*contracts.MessageData) + + if data.Properties["test"] != "OK" { + t.Error("Common properties did not propagate to telemetry") + } + + if data.Properties["no-write"] != "OK" { + t.Error("Common properties overwrote telemetry properties") + } +} + +func TestContextTags(t *testing.T) { + // Just a quick test to make sure it works. + tags := make(contracts.ContextTags) + if v := tags.Session().GetId(); v != "" { + t.Error("Failed to get empty session ID") + } + + tags.Session().SetIsFirst("true") + if v := tags.Session().GetIsFirst(); v != "true" { + t.Error("Failed to get value") + } + + if v, ok := tags["ai.session.isFirst"]; !ok || v != "true" { + t.Error("Failed to get isFirst through raw map") + } + + tags.Session().SetIsFirst("") + if v, ok := tags["ai.session.isFirst"]; ok || v != "" { + t.Error("SetIsFirst with empty string failed to remove it from the map") + } +} + +func TestSanitize(t *testing.T) { + name := strings.Repeat("Z", 1024) + val := strings.Repeat("Y", 10240) + + ev := NewEventTelemetry(name) + ev.Properties[name] = val + ev.Measurements[name] = 55.0 + + ctx := NewTelemetryContext(test_ikey) + ctx.Tags.Session().SetId(name) + + // We'll be looking for messages with these values: + found := map[string]int{ + "EventData.Name exceeded": 0, + "EventData.Properties has value": 0, + "EventData.Properties has key": 0, + "EventData.Measurements has key": 0, + "ai.session.id exceeded": 0, + } + + // Set up listener for the warnings. + NewDiagnosticsMessageListener(func(msg string) error { + for k, _ := range found { + if strings.Contains(msg, k) { + found[k] = found[k] + 1 + break + } + } + + return nil + }) + + defer resetDiagnosticsListeners() + + // This may break due to hardcoded limits... Check contracts. + envelope := ctx.envelop(ev) + + // Make sure all the warnings were found in the output + for k, v := range found { + if v != 1 { + t.Errorf("Did not find a warning containing \"%s\"", k) + } + } + + // Check the format of the stuff we found in the envelope + if v, ok := envelope.Tags[contracts.SessionId]; !ok || v != name[:64] { + t.Error("Session ID tag was not truncated") + } + + evdata := envelope.Data.(*contracts.Data).BaseData.(*contracts.EventData) + if evdata.Name != name[:512] { + t.Error("Event name was not truncated") + } + + if v, ok := evdata.Properties[name[:150]]; !ok || v != val[:8192] { + t.Error("Event property name/value was not truncated") + } + + if v, ok := evdata.Measurements[name[:150]]; !ok || v != 55.0 { + t.Error("Event measurement name was not truncated") + } +} + +func TestTimestamp(t *testing.T) { + ev := NewEventTelemetry("event") + ev.Timestamp = time.Unix(1523667421, 500000000) + + envelope := NewTelemetryContext(test_ikey).envelop(ev) + if envelope.Time != "2018-04-14T00:57:01.5Z" { + t.Errorf("Unexpected timestamp: %s", envelope.Time) + } +} diff --git a/application-insights/appinsights/throttle.go b/application-insights/appinsights/throttle.go new file mode 100644 index 0000000000..2c85800d14 --- /dev/null +++ b/application-insights/appinsights/throttle.go @@ -0,0 +1,144 @@ +package appinsights + +import ( + "time" +) + +type throttleManager struct { + msgs chan *throttleMessage +} + +type throttleMessage struct { + query bool + wait bool + throttle bool + stop bool + timestamp time.Time + result chan bool +} + +func newThrottleManager() *throttleManager { + result := &throttleManager{ + msgs: make(chan *throttleMessage), + } + + go result.run() + return result +} + +func (throttle *throttleManager) RetryAfter(t time.Time) { + throttle.msgs <- &throttleMessage{ + throttle: true, + timestamp: t, + } +} + +func (throttle *throttleManager) IsThrottled() bool { + ch := make(chan bool) + throttle.msgs <- &throttleMessage{ + query: true, + result: ch, + } + + result := <-ch + close(ch) + return result +} + +func (throttle *throttleManager) NotifyWhenReady() chan bool { + result := make(chan bool, 1) + throttle.msgs <- &throttleMessage{ + wait: true, + result: result, + } + + return result +} + +func (throttle *throttleManager) Stop() { + result := make(chan bool) + throttle.msgs <- &throttleMessage{ + stop: true, + result: result, + } + + <-result + close(result) +} + +func (throttle *throttleManager) run() { + for { + throttledUntil, ok := throttle.waitForThrottle() + if !ok { + break + } + + if !throttle.waitForReady(throttledUntil) { + break + } + } + + close(throttle.msgs) +} + +func (throttle *throttleManager) waitForThrottle() (time.Time, bool) { + for { + msg := <-throttle.msgs + if msg.query { + msg.result <- false + } else if msg.wait { + msg.result <- true + } else if msg.stop { + return time.Time{}, false + } else if msg.throttle { + return msg.timestamp, true + } + } +} + +func (throttle *throttleManager) waitForReady(throttledUntil time.Time) bool { + duration := throttledUntil.Sub(currentClock.Now()) + if duration <= 0 { + return true + } + + var notify []chan bool + + // --- Throttled and waiting --- + t := currentClock.NewTimer(duration) + + for { + select { + case <-t.C(): + for _, n := range notify { + n <- true + } + + return true + case msg := <-throttle.msgs: + if msg.query { + msg.result <- true + } else if msg.wait { + notify = append(notify, msg.result) + } else if msg.stop { + for _, n := range notify { + n <- false + } + + msg.result <- true + + return false + } else if msg.throttle { + if msg.timestamp.After(throttledUntil) { + throttledUntil = msg.timestamp + + if !t.Stop() { + <-t.C() + } + + t.Reset(throttledUntil.Sub(currentClock.Now())) + } + } + } + } +} diff --git a/application-insights/appinsights/transmitter.go b/application-insights/appinsights/transmitter.go new file mode 100644 index 0000000000..33a8be03bb --- /dev/null +++ b/application-insights/appinsights/transmitter.go @@ -0,0 +1,240 @@ +package appinsights + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "io/ioutil" + "net/http" + "sort" + "time" +) + +type transmitter interface { + Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) +} + +type httpTransmitter struct { + endpoint string + client *http.Client +} + +type transmissionResult struct { + statusCode int + retryAfter *time.Time + response *backendResponse +} + +// Structures returned by data collector +type backendResponse struct { + ItemsReceived int `json:"itemsReceived"` + ItemsAccepted int `json:"itemsAccepted"` + Errors itemTransmissionResults `json:"errors"` +} + +// This needs to be its own type because it implements sort.Interface +type itemTransmissionResults []*itemTransmissionResult + +type itemTransmissionResult struct { + Index int `json:"index"` + StatusCode int `json:"statusCode"` + Message string `json:"message"` +} + +const ( + successResponse = 200 + partialSuccessResponse = 206 + requestTimeoutResponse = 408 + tooManyRequestsResponse = 429 + tooManyRequestsOverExtendedTimeResponse = 439 + errorResponse = 500 + serviceUnavailableResponse = 503 +) + +func newTransmitter(endpointAddress string, client *http.Client) transmitter { + if client == nil { + client = http.DefaultClient + } + return &httpTransmitter{endpointAddress, client} +} + +func (transmitter *httpTransmitter) Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) { + diagnosticsWriter.Printf("--------- Transmitting %d items ---------", len(items)) + startTime := time.Now() + + // Compress the payload + var postBody bytes.Buffer + gzipWriter := gzip.NewWriter(&postBody) + if _, err := gzipWriter.Write(payload); err != nil { + diagnosticsWriter.Printf("Failed to compress the payload: %s", err.Error()) + gzipWriter.Close() + return nil, err + } + + gzipWriter.Close() + + req, err := http.NewRequest("POST", transmitter.endpoint, &postBody) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Encoding", "gzip") + req.Header.Set("Content-Type", "application/x-json-stream") + req.Header.Set("Accept-Encoding", "gzip, deflate") + + resp, err := transmitter.client.Do(req) + if err != nil { + diagnosticsWriter.Printf("Failed to transmit telemetry: %s", err.Error()) + return nil, err + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + diagnosticsWriter.Printf("Failed to read response from server: %s", err.Error()) + return nil, err + } + + duration := time.Since(startTime) + + result := &transmissionResult{statusCode: resp.StatusCode} + + // Grab Retry-After header + if retryAfterValue, ok := resp.Header[http.CanonicalHeaderKey("Retry-After")]; ok && len(retryAfterValue) == 1 { + if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterValue[0]); err == nil { + result.retryAfter = &retryAfterTime + } + } + + // Parse body, if possible + response := &backendResponse{} + if err := json.Unmarshal(body, &response); err == nil { + result.response = response + } + + // Write diagnostics + if diagnosticsWriter.hasListeners() { + diagnosticsWriter.Printf("Telemetry transmitted in %s", duration) + diagnosticsWriter.Printf("Response: %d", result.statusCode) + if result.response != nil { + diagnosticsWriter.Printf("Items accepted/received: %d/%d", result.response.ItemsAccepted, result.response.ItemsReceived) + if len(result.response.Errors) > 0 { + diagnosticsWriter.Printf("Errors:") + for _, err := range result.response.Errors { + if err.Index < len(items) { + diagnosticsWriter.Printf("#%d - %d %s", err.Index, err.StatusCode, err.Message) + diagnosticsWriter.Printf("Telemetry item:\n\t%s", string(items[err.Index:err.Index+1].serialize())) + } + } + } + } + } + + return result, nil +} + +func (result *transmissionResult) IsSuccess() bool { + return result.statusCode == successResponse || + // Partial response but all items accepted + (result.statusCode == partialSuccessResponse && + result.response != nil && + result.response.ItemsReceived == result.response.ItemsAccepted) +} + +func (result *transmissionResult) IsFailure() bool { + return result.statusCode != successResponse && result.statusCode != partialSuccessResponse +} + +func (result *transmissionResult) CanRetry() bool { + if result.IsSuccess() { + return false + } + + return result.statusCode == partialSuccessResponse || + result.retryAfter != nil || + (result.statusCode == requestTimeoutResponse || + result.statusCode == serviceUnavailableResponse || + result.statusCode == errorResponse || + result.statusCode == tooManyRequestsResponse || + result.statusCode == tooManyRequestsOverExtendedTimeResponse) +} + +func (result *transmissionResult) IsPartialSuccess() bool { + return result.statusCode == partialSuccessResponse && + result.response != nil && + result.response.ItemsReceived != result.response.ItemsAccepted +} + +func (result *transmissionResult) IsThrottled() bool { + return result.statusCode == tooManyRequestsResponse || + result.statusCode == tooManyRequestsOverExtendedTimeResponse || + result.retryAfter != nil +} + +func (result *itemTransmissionResult) CanRetry() bool { + return result.StatusCode == requestTimeoutResponse || + result.StatusCode == serviceUnavailableResponse || + result.StatusCode == errorResponse || + result.StatusCode == tooManyRequestsResponse || + result.StatusCode == tooManyRequestsOverExtendedTimeResponse +} + +func (result *transmissionResult) GetRetryItems(payload []byte, items telemetryBufferItems) ([]byte, telemetryBufferItems) { + if result.statusCode == partialSuccessResponse && result.response != nil { + // Make sure errors are ordered by index + sort.Sort(result.response.Errors) + + var resultPayload bytes.Buffer + resultItems := make(telemetryBufferItems, 0) + ptr := 0 + idx := 0 + + // Find each retryable error + for _, responseResult := range result.response.Errors { + if responseResult.CanRetry() { + // Advance ptr to start of desired line + for ; idx < responseResult.Index && ptr < len(payload); ptr++ { + if payload[ptr] == '\n' { + idx++ + } + } + + startPtr := ptr + + // Read to end of line + for ; idx == responseResult.Index && ptr < len(payload); ptr++ { + if payload[ptr] == '\n' { + idx++ + } + } + + // Copy item into output buffer + resultPayload.Write(payload[startPtr:ptr]) + resultItems = append(resultItems, items[responseResult.Index]) + } + } + + return resultPayload.Bytes(), resultItems + } else if result.CanRetry() { + return payload, items + } else { + return payload[:0], items[:0] + } +} + +// sort.Interface implementation for Errors[] list + +func (results itemTransmissionResults) Len() int { + return len(results) +} + +func (results itemTransmissionResults) Less(i, j int) bool { + return results[i].Index < results[j].Index +} + +func (results itemTransmissionResults) Swap(i, j int) { + tmp := results[i] + results[i] = results[j] + results[j] = tmp +} diff --git a/application-insights/appinsights/transmitter_test.go b/application-insights/appinsights/transmitter_test.go new file mode 100644 index 0000000000..7fe3df5042 --- /dev/null +++ b/application-insights/appinsights/transmitter_test.go @@ -0,0 +1,508 @@ +package appinsights + +import ( + "bytes" + "compress/gzip" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" +) + +type testServer struct { + server *httptest.Server + notify chan *testRequest + + responseData []byte + responseCode int + responseHeaders map[string]string +} + +type testRequest struct { + request *http.Request + body []byte +} + +func (server *testServer) Close() { + server.server.Close() + close(server.notify) +} + +func (server *testServer) ServeHTTP(writer http.ResponseWriter, req *http.Request) { + body, _ := ioutil.ReadAll(req.Body) + + hdr := writer.Header() + for k, v := range server.responseHeaders { + hdr[k] = []string{v} + } + + writer.WriteHeader(server.responseCode) + writer.Write(server.responseData) + + server.notify <- &testRequest{ + request: req, + body: body, + } +} + +func (server *testServer) waitForRequest(t *testing.T) *testRequest { + select { + case req := <-server.notify: + return req + case <-time.After(time.Second): + t.Fatal("Server did not receive request within a second") + return nil /* not reached */ + } +} + +type nullTransmitter struct{} + +func (transmitter *nullTransmitter) Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) { + return &transmissionResult{statusCode: successResponse}, nil +} + +func newTestClientServer() (transmitter, *testServer) { + server := &testServer{} + server.server = httptest.NewServer(server) + server.notify = make(chan *testRequest, 1) + server.responseCode = 200 + server.responseData = make([]byte, 0) + server.responseHeaders = make(map[string]string) + + client := newTransmitter(fmt.Sprintf("http://%s/v2/track", server.server.Listener.Addr().String()), nil) + + return client, server +} + +func newTestTlsClientServer(t *testing.T) (transmitter, *testServer) { + server := &testServer{} + server.server = httptest.NewTLSServer(server) + server.notify = make(chan *testRequest, 1) + server.responseCode = 200 + server.responseData = make([]byte, 0) + server.responseHeaders = make(map[string]string) + + client := newTransmitter(fmt.Sprintf("https://%s/v2/track", server.server.Listener.Addr().String()), server.server.Client()) + + return client, server +} + +func TestBasicTransitTls(t *testing.T) { + client, server := newTestTlsClientServer(t) + + doBasicTransmit(client, server, t) +} + +func TestBasicTransmit(t *testing.T) { + client, server := newTestClientServer() + + doBasicTransmit(client, server, t) +} + +func doBasicTransmit(client transmitter, server *testServer, t *testing.T) { + defer server.Close() + + server.responseData = []byte(`{"itemsReceived":3, "itemsAccepted":5, "errors":[]}`) + server.responseHeaders["Content-type"] = "application/json" + result, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) + if err != nil { + fmt.Println(err.Error()) + } + req := server.waitForRequest(t) + + if err != nil { + t.Errorf("err: %s", err.Error()) + } + + if req.request.Method != "POST" { + t.Error("request.Method") + } + + cencoding := req.request.Header[http.CanonicalHeaderKey("Content-Encoding")] + if len(cencoding) != 1 || cencoding[0] != "gzip" { + t.Errorf("Content-encoding: %q", cencoding) + } + + // Check for gzip magic number + if len(req.body) < 2 || req.body[0] != 0x1f || req.body[1] != 0x8b { + t.Fatal("Missing gzip magic number") + } + + // Decompress payload + reader, err := gzip.NewReader(bytes.NewReader(req.body)) + if err != nil { + t.Fatalf("Couldn't create gzip reader: %s", err.Error()) + } + + body, err := ioutil.ReadAll(reader) + reader.Close() + if err != nil { + t.Fatalf("Couldn't read compressed data: %s", err.Error()) + } + + if string(body) != "foobar" { + t.Error("body") + } + + ctype := req.request.Header[http.CanonicalHeaderKey("Content-Type")] + if len(ctype) != 1 || ctype[0] != "application/x-json-stream" { + t.Errorf("Content-type: %q", ctype) + } + + if result.statusCode != 200 { + t.Error("statusCode") + } + + if result.retryAfter != nil { + t.Error("retryAfter") + } + + if result.response == nil { + t.Fatal("response") + } + + if result.response.ItemsReceived != 3 { + t.Error("ItemsReceived") + } + + if result.response.ItemsAccepted != 5 { + t.Error("ItemsAccepted") + } + + if len(result.response.Errors) != 0 { + t.Error("response.Errors") + } +} + +func TestFailedTransmit(t *testing.T) { + client, server := newTestClientServer() + defer server.Close() + + server.responseCode = errorResponse + server.responseData = []byte(`{"itemsReceived":3, "itemsAccepted":0, "errors":[{"index": 2, "statusCode": 500, "message": "Hello"}]}`) + server.responseHeaders["Content-type"] = "application/json" + result, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) + server.waitForRequest(t) + + if err != nil { + t.Errorf("err: %s", err.Error()) + } + + if result.statusCode != errorResponse { + t.Error("statusCode") + } + + if result.retryAfter != nil { + t.Error("retryAfter") + } + + if result.response == nil { + t.Fatal("response") + } + + if result.response.ItemsReceived != 3 { + t.Error("ItemsReceived") + } + + if result.response.ItemsAccepted != 0 { + t.Error("ItemsAccepted") + } + + if len(result.response.Errors) != 1 { + t.Fatal("len(Errors)") + } + + if result.response.Errors[0].Index != 2 { + t.Error("Errors[0].index") + } + + if result.response.Errors[0].StatusCode != errorResponse { + t.Error("Errors[0].statusCode") + } + + if result.response.Errors[0].Message != "Hello" { + t.Error("Errors[0].message") + } +} + +func TestThrottledTransmit(t *testing.T) { + client, server := newTestClientServer() + defer server.Close() + + server.responseCode = errorResponse + server.responseData = make([]byte, 0) + server.responseHeaders["Content-type"] = "application/json" + server.responseHeaders["retry-after"] = "Wed, 09 Aug 2017 23:43:57 UTC" + result, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) + server.waitForRequest(t) + + if err != nil { + t.Errorf("err: %s", err.Error()) + } + + if result.statusCode != errorResponse { + t.Error("statusCode") + } + + if result.response != nil { + t.Fatal("response") + } + + if result.retryAfter == nil { + t.Fatal("retryAfter") + } + + if (*result.retryAfter).Unix() != 1502322237 { + t.Error("retryAfter.Unix") + } +} + +func TestTransmitDiagnostics(t *testing.T) { + client, server := newTestClientServer() + defer server.Close() + + var msgs []string + notify := make(chan bool, 1) + + NewDiagnosticsMessageListener(func(message string) error { + if message == "PING" { + notify <- true + } else { + msgs = append(msgs, message) + } + + return nil + }) + + defer resetDiagnosticsListeners() + + server.responseCode = errorResponse + server.responseData = []byte(`{"itemsReceived":1, "itemsAccepted":0, "errors":[{"index": 0, "statusCode": 500, "message": "Hello"}]}`) + server.responseHeaders["Content-type"] = "application/json" + _, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) + server.waitForRequest(t) + + // Wait for diagnostics to catch up. + diagnosticsWriter.Write("PING") + <-notify + + if err != nil { + t.Errorf("err: %s", err.Error()) + } + + // The last line should say "Errors:" and not include the error because the telemetry item wasn't submitted. + if !strings.Contains(msgs[len(msgs)-1], "Errors:") { + t.Errorf("Last line should say 'Errors:', with no errors listed. Instead: %s", msgs[len(msgs)-1]) + } + + // Go again but include telemetry items this time. + server.responseCode = errorResponse + server.responseData = []byte(`{"itemsReceived":1, "itemsAccepted":0, "errors":[{"index": 0, "statusCode": 500, "message": "Hello"}]}`) + server.responseHeaders["Content-type"] = "application/json" + _, err = client.Transmit([]byte("foobar"), telemetryBuffer(NewTraceTelemetry("World", Warning))) + server.waitForRequest(t) + + // Wait for diagnostics to catch up. + diagnosticsWriter.Write("PING") + <-notify + + if err != nil { + t.Errorf("err: %s", err.Error()) + } + + if !strings.Contains(msgs[len(msgs)-2], "500 Hello") { + t.Error("Telemetry error should be prefaced with result code and message") + } + + if !strings.Contains(msgs[len(msgs)-1], "World") { + t.Error("Raw telemetry item should be found on last line") + } + + close(notify) +} + +type resultProperties struct { + isSuccess bool + isFailure bool + canRetry bool + isThrottled bool + isPartialSuccess bool + retryableErrors bool +} + +func checkTransmitResult(t *testing.T, result *transmissionResult, expected *resultProperties) { + retryAfter := "" + if result.retryAfter != nil { + retryAfter = (*result.retryAfter).String() + } + response := "" + if result.response != nil { + response = fmt.Sprintf("%v", *result.response) + } + id := fmt.Sprintf("%d, retryAfter:%s, response:%s", result.statusCode, retryAfter, response) + + if result.IsSuccess() != expected.isSuccess { + t.Errorf("Expected IsSuccess() == %t [%s]", expected.isSuccess, id) + } + + if result.IsFailure() != expected.isFailure { + t.Errorf("Expected IsFailure() == %t [%s]", expected.isFailure, id) + } + + if result.CanRetry() != expected.canRetry { + t.Errorf("Expected CanRetry() == %t [%s]", expected.canRetry, id) + } + + if result.IsThrottled() != expected.isThrottled { + t.Errorf("Expected IsThrottled() == %t [%s]", expected.isThrottled, id) + } + + if result.IsPartialSuccess() != expected.isPartialSuccess { + t.Errorf("Expected IsPartialSuccess() == %t [%s]", expected.isPartialSuccess, id) + } + + // retryableErrors is true if CanRetry() and any error is recoverable + retryableErrors := false + if result.CanRetry() && result.response != nil { + for _, err := range result.response.Errors { + if err.CanRetry() { + retryableErrors = true + } + } + } + + if retryableErrors != expected.retryableErrors { + t.Errorf("Expected any(Errors.CanRetry) == %t [%s]", expected.retryableErrors, id) + } +} + +func TestTransmitResults(t *testing.T) { + retryAfter := time.Unix(1502322237, 0) + partialNoRetries := &backendResponse{ + ItemsAccepted: 3, + ItemsReceived: 5, + Errors: []*itemTransmissionResult{ + &itemTransmissionResult{Index: 2, StatusCode: 400, Message: "Bad 1"}, + &itemTransmissionResult{Index: 4, StatusCode: 400, Message: "Bad 2"}, + }, + } + + partialSomeRetries := &backendResponse{ + ItemsAccepted: 2, + ItemsReceived: 4, + Errors: []*itemTransmissionResult{ + &itemTransmissionResult{Index: 2, StatusCode: 400, Message: "Bad 1"}, + &itemTransmissionResult{Index: 4, StatusCode: 408, Message: "OK Later"}, + }, + } + + noneAccepted := &backendResponse{ + ItemsAccepted: 0, + ItemsReceived: 5, + Errors: []*itemTransmissionResult{ + &itemTransmissionResult{Index: 0, StatusCode: 500, Message: "Bad 1"}, + &itemTransmissionResult{Index: 1, StatusCode: 500, Message: "Bad 2"}, + &itemTransmissionResult{Index: 2, StatusCode: 500, Message: "Bad 3"}, + &itemTransmissionResult{Index: 3, StatusCode: 500, Message: "Bad 4"}, + &itemTransmissionResult{Index: 4, StatusCode: 500, Message: "Bad 5"}, + }, + } + + allAccepted := &backendResponse{ + ItemsAccepted: 6, + ItemsReceived: 6, + Errors: make([]*itemTransmissionResult, 0), + } + + checkTransmitResult(t, &transmissionResult{200, nil, allAccepted}, + &resultProperties{isSuccess: true}) + checkTransmitResult(t, &transmissionResult{206, nil, partialSomeRetries}, + &resultProperties{isPartialSuccess: true, canRetry: true, retryableErrors: true}) + checkTransmitResult(t, &transmissionResult{206, nil, partialNoRetries}, + &resultProperties{isPartialSuccess: true, canRetry: true}) + checkTransmitResult(t, &transmissionResult{206, nil, noneAccepted}, + &resultProperties{isPartialSuccess: true, canRetry: true, retryableErrors: true}) + checkTransmitResult(t, &transmissionResult{206, nil, allAccepted}, + &resultProperties{isSuccess: true}) + checkTransmitResult(t, &transmissionResult{400, nil, nil}, + &resultProperties{isFailure: true}) + checkTransmitResult(t, &transmissionResult{408, nil, nil}, + &resultProperties{isFailure: true, canRetry: true}) + checkTransmitResult(t, &transmissionResult{408, &retryAfter, nil}, + &resultProperties{isFailure: true, canRetry: true, isThrottled: true}) + checkTransmitResult(t, &transmissionResult{429, nil, nil}, + &resultProperties{isFailure: true, canRetry: true, isThrottled: true}) + checkTransmitResult(t, &transmissionResult{429, &retryAfter, nil}, + &resultProperties{isFailure: true, canRetry: true, isThrottled: true}) + checkTransmitResult(t, &transmissionResult{500, nil, nil}, + &resultProperties{isFailure: true, canRetry: true}) + checkTransmitResult(t, &transmissionResult{503, nil, nil}, + &resultProperties{isFailure: true, canRetry: true}) + checkTransmitResult(t, &transmissionResult{401, nil, nil}, + &resultProperties{isFailure: true}) + checkTransmitResult(t, &transmissionResult{408, nil, partialSomeRetries}, + &resultProperties{isFailure: true, canRetry: true, retryableErrors: true}) + checkTransmitResult(t, &transmissionResult{500, nil, partialSomeRetries}, + &resultProperties{isFailure: true, canRetry: true, retryableErrors: true}) +} + +func TestGetRetryItems(t *testing.T) { + mockClock() + defer resetClock() + + // Keep a pristine copy. + originalPayload, originalItems := makePayload() + + res1 := &transmissionResult{ + statusCode: 200, + response: &backendResponse{ItemsReceived: 7, ItemsAccepted: 7}, + } + + payload1, items1 := res1.GetRetryItems(makePayload()) + if len(payload1) > 0 || len(items1) > 0 { + t.Error("GetRetryItems shouldn't return anything") + } + + res2 := &transmissionResult{statusCode: 408} + + payload2, items2 := res2.GetRetryItems(makePayload()) + if string(originalPayload) != string(payload2) || len(items2) != 7 { + t.Error("GetRetryItems shouldn't return anything") + } + + res3 := &transmissionResult{ + statusCode: 206, + response: &backendResponse{ + ItemsReceived: 7, + ItemsAccepted: 4, + Errors: []*itemTransmissionResult{ + &itemTransmissionResult{Index: 1, StatusCode: 200, Message: "OK"}, + &itemTransmissionResult{Index: 3, StatusCode: 400, Message: "Bad"}, + &itemTransmissionResult{Index: 5, StatusCode: 408, Message: "Later"}, + &itemTransmissionResult{Index: 6, StatusCode: 500, Message: "Oops"}, + }, + }, + } + + payload3, items3 := res3.GetRetryItems(makePayload()) + expected3 := telemetryBufferItems{originalItems[5], originalItems[6]} + if string(payload3) != string(expected3.serialize()) || len(items3) != 2 { + t.Error("Unexpected result") + } +} + +func makePayload() ([]byte, telemetryBufferItems) { + buffer := telemetryBuffer() + for i := 0; i < 7; i++ { + tr := NewTraceTelemetry(fmt.Sprintf("msg%d", i+1), contracts.SeverityLevel(i%5)) + tr.Tags.Operation().SetId(fmt.Sprintf("op%d", i)) + buffer.add(tr) + } + + return buffer.serialize(), buffer +} diff --git a/application-insights/appinsights/uuid.go b/application-insights/appinsights/uuid.go new file mode 100644 index 0000000000..08c6786f09 --- /dev/null +++ b/application-insights/appinsights/uuid.go @@ -0,0 +1,72 @@ +package appinsights + +import ( + crand "crypto/rand" + "encoding/binary" + "io" + "math/rand" + "sync" + "time" + + "github.com/gofrs/uuid" +) + +// uuidGenerator is a wrapper for gofrs/uuid, an active fork of satori/go.uuid used for a few reasons: +// - Avoids build failures due to version differences when a project imports us but +// does not respect our vendoring. (satori/go.uuid#77, #71, #66, ...) +// - Avoids error output when creaing new UUID's: if the crypto reader fails, +// this will fallback on the standard library PRNG, since this is never used +// for a sensitive application. +// - Uses io.ReadFull to guarantee fully-populated UUID's (satori/go.uuid#73) +type uuidGenerator struct { + sync.Mutex + fallbackRand *rand.Rand + reader io.Reader +} + +var uuidgen *uuidGenerator = newUuidGenerator(crand.Reader) + +// newUuidGenerator creates a new uuiGenerator with the specified crypto random reader. +func newUuidGenerator(reader io.Reader) *uuidGenerator { + // Setup seed for fallback random generator + var seed int64 + b := make([]byte, 8) + if _, err := io.ReadFull(reader, b); err == nil { + seed = int64(binary.BigEndian.Uint64(b)) + } else { + // Otherwise just use the timestamp + seed = time.Now().UTC().UnixNano() + } + + return &uuidGenerator{ + reader: reader, + fallbackRand: rand.New(rand.NewSource(seed)), + } +} + +// newUUID generates a new V4 UUID +func (gen *uuidGenerator) newUUID() uuid.UUID { + //call the standard generator + u, err := uuid.NewV4() + //err will be either EOF or unexpected EOF + if err != nil { + gen.fallback(&u) + } + + return u +} + +// fallback populates the specified UUID with the standard library's PRNG +func (gen *uuidGenerator) fallback(u *uuid.UUID) { + gen.Lock() + defer gen.Unlock() + // This does not fail as per documentation + gen.fallbackRand.Read(u[:]) + u.SetVersion(uuid.V4) + u.SetVariant(uuid.VariantRFC4122) +} + +// newUUID generates a new V4 UUID +func newUUID() uuid.UUID { + return uuidgen.newUUID() +} diff --git a/application-insights/appinsights/uuid_test.go b/application-insights/appinsights/uuid_test.go new file mode 100644 index 0000000000..ebd6d1dee1 --- /dev/null +++ b/application-insights/appinsights/uuid_test.go @@ -0,0 +1,65 @@ +package appinsights + +import ( + "io" + "sync" + "testing" +) + +func TestNewUUID(t *testing.T) { + var start sync.WaitGroup + var finish sync.WaitGroup + + start.Add(1) + + goroutines := 250 + uuidsPerRoutine := 10 + results := make(chan string, 100) + + // Start normal set of UUID generation: + for i := 0; i < goroutines; i++ { + finish.Add(1) + go func() { + defer finish.Done() + start.Wait() + for t := 0; t < uuidsPerRoutine; t++ { + results <- newUUID().String() + } + }() + } + + // Start broken set of UUID generation + brokenGen := newUuidGenerator(&brokenReader{}) + for i := 0; i < goroutines; i++ { + finish.Add(1) + go func() { + defer finish.Done() + start.Wait() + for t := 0; t < uuidsPerRoutine; t++ { + results <- brokenGen.newUUID().String() + } + }() + } + + // Close the channel when all the goroutines have exited + go func() { + finish.Wait() + close(results) + }() + + used := make(map[string]bool) + start.Done() + for id := range results { + if _, ok := used[id]; ok { + t.Errorf("UUID was generated twice: %s", id) + } + + used[id] = true + } +} + +type brokenReader struct{} + +func (reader *brokenReader) Read(b []byte) (int, error) { + return 0, io.EOF +} diff --git a/application-insights/generateschema.ps1 b/application-insights/generateschema.ps1 new file mode 100644 index 0000000000..0300a16387 --- /dev/null +++ b/application-insights/generateschema.ps1 @@ -0,0 +1,92 @@ +<# + +.SYNOPSIS + +Generate data contracts for Application Insights Go SDK from bond schema. + +.DESCRIPTION + +This is a convenience tool for generating the AI data contracts from the latest +bond schema. It requires BondSchemaGenerator.exe in order to operate. It also +requires the latest bond schema, but will check it out from github if it is not +present in the current directory. + +.PARAMETER BondSchemaGenerator + +The full path to BondSchemaGenerator.exe + +.PARAMETER SchemasDir + +The path to the directory that contains all of the input .bond files. + +.LINK https://github.com/microsoft/ApplicationInsights-Home + +#> + +[cmdletbinding()] +Param( + [Parameter(Mandatory=$true)] + [string] $BondSchemaGenerator, + [string] $SchemasDir +) + +function RunBondSchemaGenerator +{ + [cmdletbinding()] + Param( + [string] $Language, + [string[]] $Files, + [string] $Layout, + [string[]] $Omissions + ) + + $args = @("-v") + $args += @("-o", ".") + $args += @("-e", $Language) + $args += @("-t", $Layout) + + foreach ($file in $Files) { + $args += @("-i", $file) + } + + foreach ($omission in $Omissions) { + $args += @("--omit", $omission) + } + + & "$BondSchemaGenerator" $args 2>&1 +} + +$origpath = Get-Location + +try { + $scriptpath = $MyInvocation.MyCommand.Path + $dir = Split-Path $scriptpath + cd $dir + + if (-not (Test-Path $BondSchemaGenerator -PathType Leaf)) { + Write-Host "Could not find BondSchemaGenerator at $BondSchemaGenerator" + Write-Host "Please specify the full path" + Exit 1 + } + + if (-not $schemasDir) { + $schemasDir = ".\ApplicationInsights-Home\EndpointSpecs\Schemas\Bond" + + # Check locally. + if (-not (Test-Path .\ApplicationInsights-Home -PathType Container)) { + # Clone into it! + git clone https://github.com/microsoft/ApplicationInsights-Home.git + } + } + + $files = Get-ChildItem $schemasDir | % { "$schemasDir\$_" } + $omissions = @("Microsoft.Telemetry.Domain", "Microsoft.Telemetry.Base", "AI.AjaxCallData", "AI.PageViewPerfData") + + RunBondSchemaGenerator -Files $files -Language GoBondTemplateLanguage -Layout GoTemplateLayout -Omissions $omissions + RunBondSchemaGenerator -Files $files -Language GoContextTagsLanguage -Layout GoTemplateLayout -Omissions $omissions + + cd appinsights\contracts + go fmt +} finally { + cd $origpath +} diff --git a/application-insights/go.mod b/application-insights/go.mod new file mode 100644 index 0000000000..78cee283e3 --- /dev/null +++ b/application-insights/go.mod @@ -0,0 +1,15 @@ +module github.com/microsoft/ApplicationInsights-Go + +go 1.24.4 + +require ( + code.cloudfoundry.org/clock v1.41.0 + github.com/gofrs/uuid v4.4.0+incompatible + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/application-insights/go.sum b/application-insights/go.sum new file mode 100644 index 0000000000..4566dc987c --- /dev/null +++ b/application-insights/go.sum @@ -0,0 +1,38 @@ +code.cloudfoundry.org/clock v1.41.0 h1:YiYQSEqcxswK+YtQ+NRIE31E1VNXkwb53Bb3zRmsoOM= +code.cloudfoundry.org/clock v1.41.0/go.mod h1:ncX4UpMuVwZooK7Rw7P+fsE2brLasFbPlibOOrZq40w= +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.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tedsuo/ifrit v0.0.0-20230516164442-7862c310ad26 h1:mWCRvpoEMVlslxEvvptKgIUb35va9yj9Oq5wGw/er5I= +github.com/tedsuo/ifrit v0.0.0-20230516164442-7862c310ad26/go.mod h1:0uD3VMXkZ7Bw0ojGCwDzebBBzPBXtzEZeXai+56BLX4= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index ebf3cbf9a8..66e2991719 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Azure/azure-container-networking -go 1.23.2 +go 1.24.4 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 @@ -22,10 +22,9 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/hashicorp/go-version v1.7.0 - github.com/microsoft/ApplicationInsights-Go v0.4.4 github.com/nxadm/tail v1.4.11 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.34.1 + github.com/onsi/gomega v1.37.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.22.0 @@ -52,7 +51,7 @@ require ( ) require ( - code.cloudfoundry.org/clock v1.0.0 // indirect + code.cloudfoundry.org/clock v1.41.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect @@ -62,11 +61,11 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/fsnotify/fsnotify v1.9.0 - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.22.7 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -128,6 +127,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 github.com/cilium/cilium v1.15.16 github.com/jsternberg/zap-logfmt v1.3.0 + github.com/microsoft/ApplicationInsights-Go v0.4.4 golang.org/x/sync v0.15.0 gotest.tools/v3 v3.5.2 k8s.io/kubectl v0.28.5 @@ -250,7 +250,7 @@ require ( ) replace ( - github.com/microsoft/ApplicationInsights-Go => github.com/beegiik/ApplicationInsights-Go v1.8.0 + github.com/microsoft/ApplicationInsights-Go => ./application-insights github.com/onsi/ginkgo => github.com/onsi/ginkgo v1.12.0 github.com/onsi/gomega => github.com/onsi/gomega v1.10.0 ) diff --git a/go.sum b/go.sum index a9b15a0df3..c09df00e3b 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,16 @@ cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +<<<<<<< HEAD cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= code.cloudfoundry.org/clock v1.0.0 h1:kFXWQM4bxYvdBw2X8BbBeXwQNgfoWv1vqAk2ZZyBN2o= code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= +======= +code.cloudfoundry.org/clock v1.41.0 h1:YiYQSEqcxswK+YtQ+NRIE31E1VNXkwb53Bb3zRmsoOM= +code.cloudfoundry.org/clock v1.41.0/go.mod h1:ncX4UpMuVwZooK7Rw7P+fsE2brLasFbPlibOOrZq40w= +>>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-container-networking/zapai v0.0.3 h1:73druF1cnne5Ign/ztiXP99Ss5D+UJ80EL2mzPgNRhk= @@ -67,8 +72,6 @@ github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu github.com/avast/retry-go/v3 v3.1.1/go.mod h1:6cXRK369RpzFL3UQGqIUp9Q7GDrams+KsYWrfNA1/nQ= github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= -github.com/beegiik/ApplicationInsights-Go v1.8.0 h1:9eQ7wk7o03GA7HM/oDSOf/STOq5YA09cJfUiZa0yobU= -github.com/beegiik/ApplicationInsights-Go v1.8.0/go.mod h1:wGv9tvjn4hfY0O95MzNM7ftYfphBi0BGqknkBdFF/cM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/billgraziano/dpapi v0.5.0 h1:pcxA17vyjbDqYuxCFZbgL9tYIk2xgbRZjRaIbATwh+8= @@ -169,8 +172,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -271,8 +274,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -396,8 +399,8 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.10.0 h1:Gwkk+PTu/nfOwNMtUB/mRUv0X7ewW5dO4AERT1ThVKo= github.com/onsi/gomega v1.10.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -477,10 +480,13 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= <<<<<<< HEAD +<<<<<<< HEAD github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= ======= >>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +======= +>>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -489,14 +495,16 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +<<<<<<< HEAD github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= +======= +>>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= @@ -571,8 +579,13 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +<<<<<<< HEAD go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +======= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +>>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -685,8 +698,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 6b45eace1078ac4dd67667ac3f6b9107c5bcd8b8 Mon Sep 17 00:00:00 2001 From: beegiik Date: Tue, 15 Jul 2025 16:35:09 +0100 Subject: [PATCH 3/9] feat: Create connection string helper function and update telemetry handle --- aitelemetry/connection_string_parser.go | 45 ++ aitelemetry/connection_string_parser_test.go | 66 ++ aitelemetry/telemetrywrapper.go | 9 +- aitelemetry/telemetrywrapper_test.go | 2 +- application-insights/.gitignore | 31 - application-insights/.travis.yml | 15 - application-insights/CODE_OF_CONDUCT.md | 1 - application-insights/CONTRIBUTING.md | 4 - application-insights/LICENSE | 22 - application-insights/PULL_REQUEST_TEMPLATE.md | 8 - application-insights/README.md | 557 --------------- application-insights/SECURITY.md | 41 -- application-insights/appinsights/client.go | 155 ----- .../appinsights/client_test.go | 236 ------- application-insights/appinsights/clock.go | 11 - .../appinsights/clock_test.go | 39 -- .../appinsights/configuration.go | 68 -- .../appinsights/configuration_test.go | 38 - .../appinsights/connection_string_parser.go | 39 -- .../connection_string_parser_test.go | 66 -- application-insights/appinsights/constants.go | 20 - .../appinsights/contracts/availabilitydata.go | 111 --- .../appinsights/contracts/base.go | 25 - .../appinsights/contracts/contexttagkeys.go | 153 ---- .../appinsights/contracts/contexttags.go | 565 --------------- .../appinsights/contracts/data.go | 25 - .../appinsights/contracts/datapoint.go | 54 -- .../appinsights/contracts/datapointtype.go | 22 - .../appinsights/contracts/domain.go | 21 - .../appinsights/contracts/envelope.go | 82 --- .../appinsights/contracts/eventdata.go | 82 --- .../appinsights/contracts/exceptiondata.go | 93 --- .../appinsights/contracts/exceptiondetails.go | 66 -- .../appinsights/contracts/messagedata.go | 72 -- .../appinsights/contracts/metricdata.go | 68 -- .../appinsights/contracts/package.go | 4 - .../appinsights/contracts/pageviewdata.go | 85 --- .../contracts/remotedependencydata.go | 134 ---- .../appinsights/contracts/requestdata.go | 125 ---- .../appinsights/contracts/severitylevel.go | 31 - .../appinsights/contracts/stackframe.go | 52 -- .../appinsights/diagnostics.go | 88 --- .../appinsights/diagnostics_test.go | 120 ---- application-insights/appinsights/exception.go | 150 ---- .../appinsights/exception_test.go | 193 ------ .../appinsights/inmemorychannel.go | 449 ------------ .../appinsights/inmemorychannel_test.go | 628 ----------------- .../appinsights/jsonserializer.go | 25 - .../appinsights/jsonserializer_test.go | 483 ------------- application-insights/appinsights/package.go | 8 - application-insights/appinsights/telemetry.go | 652 ------------------ .../appinsights/telemetry_test.go | 368 ---------- .../appinsights/telemetrychannel.go | 51 -- .../appinsights/telemetrycontext.go | 104 --- .../appinsights/telemetrycontext_test.go | 145 ---- application-insights/appinsights/throttle.go | 144 ---- .../appinsights/transmitter.go | 240 ------- .../appinsights/transmitter_test.go | 508 -------------- application-insights/appinsights/uuid.go | 72 -- application-insights/appinsights/uuid_test.go | 65 -- application-insights/generateschema.ps1 | 92 --- application-insights/go.mod | 15 - application-insights/go.sum | 38 - go.mod | 4 +- go.sum | 54 +- 65 files changed, 172 insertions(+), 7867 deletions(-) create mode 100644 aitelemetry/connection_string_parser.go create mode 100644 aitelemetry/connection_string_parser_test.go delete mode 100644 application-insights/.gitignore delete mode 100644 application-insights/.travis.yml delete mode 100644 application-insights/CODE_OF_CONDUCT.md delete mode 100644 application-insights/CONTRIBUTING.md delete mode 100644 application-insights/LICENSE delete mode 100644 application-insights/PULL_REQUEST_TEMPLATE.md delete mode 100644 application-insights/README.md delete mode 100644 application-insights/SECURITY.md delete mode 100644 application-insights/appinsights/client.go delete mode 100644 application-insights/appinsights/client_test.go delete mode 100644 application-insights/appinsights/clock.go delete mode 100644 application-insights/appinsights/clock_test.go delete mode 100644 application-insights/appinsights/configuration.go delete mode 100644 application-insights/appinsights/configuration_test.go delete mode 100644 application-insights/appinsights/connection_string_parser.go delete mode 100644 application-insights/appinsights/connection_string_parser_test.go delete mode 100644 application-insights/appinsights/constants.go delete mode 100644 application-insights/appinsights/contracts/availabilitydata.go delete mode 100644 application-insights/appinsights/contracts/base.go delete mode 100644 application-insights/appinsights/contracts/contexttagkeys.go delete mode 100644 application-insights/appinsights/contracts/contexttags.go delete mode 100644 application-insights/appinsights/contracts/data.go delete mode 100644 application-insights/appinsights/contracts/datapoint.go delete mode 100644 application-insights/appinsights/contracts/datapointtype.go delete mode 100644 application-insights/appinsights/contracts/domain.go delete mode 100644 application-insights/appinsights/contracts/envelope.go delete mode 100644 application-insights/appinsights/contracts/eventdata.go delete mode 100644 application-insights/appinsights/contracts/exceptiondata.go delete mode 100644 application-insights/appinsights/contracts/exceptiondetails.go delete mode 100644 application-insights/appinsights/contracts/messagedata.go delete mode 100644 application-insights/appinsights/contracts/metricdata.go delete mode 100644 application-insights/appinsights/contracts/package.go delete mode 100644 application-insights/appinsights/contracts/pageviewdata.go delete mode 100644 application-insights/appinsights/contracts/remotedependencydata.go delete mode 100644 application-insights/appinsights/contracts/requestdata.go delete mode 100644 application-insights/appinsights/contracts/severitylevel.go delete mode 100644 application-insights/appinsights/contracts/stackframe.go delete mode 100644 application-insights/appinsights/diagnostics.go delete mode 100644 application-insights/appinsights/diagnostics_test.go delete mode 100644 application-insights/appinsights/exception.go delete mode 100644 application-insights/appinsights/exception_test.go delete mode 100644 application-insights/appinsights/inmemorychannel.go delete mode 100644 application-insights/appinsights/inmemorychannel_test.go delete mode 100644 application-insights/appinsights/jsonserializer.go delete mode 100644 application-insights/appinsights/jsonserializer_test.go delete mode 100644 application-insights/appinsights/package.go delete mode 100644 application-insights/appinsights/telemetry.go delete mode 100644 application-insights/appinsights/telemetry_test.go delete mode 100644 application-insights/appinsights/telemetrychannel.go delete mode 100644 application-insights/appinsights/telemetrycontext.go delete mode 100644 application-insights/appinsights/telemetrycontext_test.go delete mode 100644 application-insights/appinsights/throttle.go delete mode 100644 application-insights/appinsights/transmitter.go delete mode 100644 application-insights/appinsights/transmitter_test.go delete mode 100644 application-insights/appinsights/uuid.go delete mode 100644 application-insights/appinsights/uuid_test.go delete mode 100644 application-insights/generateschema.ps1 delete mode 100644 application-insights/go.mod delete mode 100644 application-insights/go.sum diff --git a/aitelemetry/connection_string_parser.go b/aitelemetry/connection_string_parser.go new file mode 100644 index 0000000000..e8db7dc031 --- /dev/null +++ b/aitelemetry/connection_string_parser.go @@ -0,0 +1,45 @@ +package aitelemetry + +import ( + "fmt" + "strings" +) + +type connectionVars struct { + InstrumentationKey string + IngestionUrl string +} + +func parseConnectionString(connectionString string) (*connectionVars, error) { + connectionVars := &connectionVars{} + + if connectionString == "" { + return nil, fmt.Errorf("Connection string cannot be empty") + } + + pairs := strings.Split(connectionString, ";") + for _, pair := range pairs { + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("Invalid connection string format: %s", pair) + } + key, value := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]) + + if key == "" { + return nil, fmt.Errorf("Key in connection string cannot be empty") + } + + switch strings.ToLower(key) { + case "instrumentationkey": + connectionVars.InstrumentationKey = value + case "ingestionendpoint": + connectionVars.IngestionUrl = value + "v2.1/track" + } + } + + if connectionVars.InstrumentationKey == "" || connectionVars.IngestionUrl == "" { + return nil, fmt.Errorf("Missing required fields in connection string") + } + + return connectionVars, nil +} diff --git a/aitelemetry/connection_string_parser_test.go b/aitelemetry/connection_string_parser_test.go new file mode 100644 index 0000000000..3044bf41d1 --- /dev/null +++ b/aitelemetry/connection_string_parser_test.go @@ -0,0 +1,66 @@ +package aitelemetry + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const connectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" + +func TestParseConnectionString(t *testing.T) { + tests := []struct { + name string + connectionString string + want *connectionVars + wantErr bool + }{ + { + name: "Valid connection string and instrumentation key", + connectionString: connectionString, + want: &connectionVars{ + InstrumentationKey: "00000000-0000-0000-0000-000000000000", + IngestionUrl: "https://ingestion.endpoint.com/v2.1/track", + }, + wantErr: false, + }, + { + name: "Invalid connection string format", + connectionString: "InvalidConnectionString", + want: nil, + wantErr: true, + }, + { + name: "Valid instrumentation key with missing ingestion endpoint", + connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=;", + want: nil, + wantErr: true, + }, + { + name: "Missing instrumentation key with valid ingestion endpoint", + connectionString: "InstrumentationKey=;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;", + want: nil, + wantErr: true, + }, + { + name: "Empty connection string", + connectionString: "", + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseConnectionString(tt.connectionString) + if tt.wantErr { + require.Error(t, err, "Expected error but got none") + } else { + require.NoError(t, err, "Expected no error but got one") + require.NotNil(t, got, "Expected a non-nil result") + require.Equal(t, tt.want.InstrumentationKey, got.InstrumentationKey, "Instrumentation Key does not match") + require.Equal(t, tt.want.IngestionUrl, got.IngestionUrl, "Ingestion URL does not match") + } + }) + } +} diff --git a/aitelemetry/telemetrywrapper.go b/aitelemetry/telemetrywrapper.go index ba4362ad97..7e1a0fc6ac 100644 --- a/aitelemetry/telemetrywrapper.go +++ b/aitelemetry/telemetrywrapper.go @@ -228,7 +228,14 @@ func NewAITelemetryWithConnectionString( setAIConfigDefaults(&aiConfig) - telemetryConfig := appinsights.NewTelemetryConfigurationWithConnectionString(cString) + connectionVars, err := parseConnectionString(cString) + if err != nil { + debugLog("Error parsing connection string: %v", err) + return nil, err + } + + telemetryConfig := appinsights.NewTelemetryConfiguration(connectionVars.InstrumentationKey) + telemetryConfig.EndpointUrl = connectionVars.IngestionUrl telemetryConfig.MaxBatchSize = aiConfig.BatchSize telemetryConfig.MaxBatchInterval = time.Duration(aiConfig.BatchInterval) * time.Second diff --git a/aitelemetry/telemetrywrapper_test.go b/aitelemetry/telemetrywrapper_test.go index 774e3ef26e..21a41a79d6 100644 --- a/aitelemetry/telemetrywrapper_test.go +++ b/aitelemetry/telemetrywrapper_test.go @@ -19,7 +19,7 @@ var ( hostAgentUrl = "localhost:3501" getCloudResponse = "AzurePublicCloud" httpURL = "http://" + hostAgentUrl - connectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" + // connectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" ) func TestMain(m *testing.M) { diff --git a/application-insights/.gitignore b/application-insights/.gitignore deleted file mode 100644 index 69d352625b..0000000000 --- a/application-insights/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test -.idea - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof - -# OS specific -.DS_Store - -# Backup filese -*~ diff --git a/application-insights/.travis.yml b/application-insights/.travis.yml deleted file mode 100644 index 26a63095a0..0000000000 --- a/application-insights/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go - -go: - - 1.13.x - - 1.14.x - - 1.15.x - -sudo: false - -os: - - osx - - linux - -script: - - go test -v ./appinsights/... diff --git a/application-insights/CODE_OF_CONDUCT.md b/application-insights/CODE_OF_CONDUCT.md deleted file mode 100644 index 65c8a42b8d..0000000000 --- a/application-insights/CODE_OF_CONDUCT.md +++ /dev/null @@ -1 +0,0 @@ -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/application-insights/CONTRIBUTING.md b/application-insights/CONTRIBUTING.md deleted file mode 100644 index dfa775d0a4..0000000000 --- a/application-insights/CONTRIBUTING.md +++ /dev/null @@ -1,4 +0,0 @@ -# How to Contribute - -If you're interested in contributing, take a look at the general [contributer's guide](https://github.com/microsoft/ApplicationInsights-Home/blob/master/CONTRIBUTING.md) first. - diff --git a/application-insights/LICENSE b/application-insights/LICENSE deleted file mode 100644 index 01d022c227..0000000000 --- a/application-insights/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015-2017 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/application-insights/PULL_REQUEST_TEMPLATE.md b/application-insights/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 6b379938de..0000000000 --- a/application-insights/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Fix # . - - -For significant contributions please make sure you have completed the following items: - -- [ ] Design discussion issue # -- [ ] Changes in public surface reviewed -- [ ] CHANGELOG.md updated diff --git a/application-insights/README.md b/application-insights/README.md deleted file mode 100644 index fb9ec325c4..0000000000 --- a/application-insights/README.md +++ /dev/null @@ -1,557 +0,0 @@ -# Microsoft Application Insights SDK for Go - -This project provides a Go SDK for Application Insights. -[Application Insights](http://azure.microsoft.com/en-us/services/application-insights/) -is a service that allows developers to keep their applications available, -performant, and successful. This go package will allow you to send -telemetry of various kinds (event, metric, trace) to the Application -Insights service where they can be visualized in the Azure Portal. - -## Status -This SDK is NOT currently maintained or supported, by Azure Container Networking or Microsoft. Azure Monitor only provides support when using our [supported SDKs](https://docs.microsoft.com/en-us/azure/azure-monitor/app/platforms#unsupported-community-sdks), and this SDK does not yet meet that standard. - -Known gaps include: -* Operation correlation is not supported, but this can be managed by the - caller through the interfaces that exist today. -* Sampling is not supported. The more mature SDKs support dynamic sampling, - but at present this does not even support manual sampling. -* Automatic collection of events is not supported. All telemetry must be - explicitly collected and sent by the user. -* Offline storage of telemetry is not supported. The .Net SDK is capable of - spilling events to disk in case of network interruption. This SDK has no - such feature. - -We’re constantly assessing opportunities to expand our support for other languages, so follow our [Azure Updates](https://azure.microsoft.com/updates/?query=application%20insights) page to receive the latest SDK news. - -## Requirements -**Install** -``` -go get github.com/microsoft/ApplicationInsights-Go/appinsights -``` -**Get an instrumentation key** ->**Note**: an instrumentation key is required before any data can be sent. Please see the "[Getting an Application Insights Instrumentation Key](https://github.com/microsoft/AppInsights-Home/wiki#getting-an-application-insights-instrumentation-key)" section of the wiki for more information. To try the SDK without an instrumentation key, set the instrumentationKey config value to a non-empty string. - -# Usage - -## Setup - -To start tracking telemetry, you'll want to first initialize a -[telemetry client](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryClient). - -```go -import "github.com/microsoft/ApplicationInsights-Go/appinsights" - -func main() { - client := appinsights.NewTelemetryClient("") -} -``` - -If you want more control over the client's behavior, you should initialize a -new [TelemetryConfiguration](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryConfiguration) -object and use it to create a client: - -```go -import "time" -import "github.com/microsoft/ApplicationInsights-Go/appinsights" - -func main() { - telemetryConfig := appinsights.NewTelemetryConfiguration("") - - // Configure how many items can be sent in one call to the data collector: - telemetryConfig.MaxBatchSize = 8192 - - // Configure the maximum delay before sending queued telemetry: - telemetryConfig.MaxBatchInterval = 2 * time.Second - - client := appinsights.NewTelemetryClientFromConfig(telemetryConfig) -} -``` - -This client will be used to submit all of your telemetry to Application -Insights. This SDK does not presently collect any telemetry automatically, -so you will use this client extensively to report application health and -status. You may want to store it in a global variable or otherwise include -it in your data model. - -## Telemetry submission - -The [TelemetryClient](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryClient) -itself has several methods for submitting telemetry: - -```go -type TelemetryClient interface { - // (much omitted) - - // Log a user action with the specified name - TrackEvent(name string) - - // Log a numeric value that is not specified with a specific event. - // Typically used to send regular reports of performance indicators. - TrackMetric(name string, value float64) - - // Log a trace message with the specified severity level. - TrackTrace(name string, severity contracts.SeverityLevel) - - // Log an HTTP request with the specified method, URL, duration and - // response code. - TrackRequest(method, url string, duration time.Duration, responseCode string) - - // Log a dependency with the specified name, type, target, and - // success status. - TrackRemoteDependency(name, dependencyType, target string, success bool) - - // Log an availability test result with the specified test name, - // duration, and success status. - TrackAvailability(name string, duration time.Duration, success bool) - - // Log an exception with the specified error, which may be a string, - // error or Stringer. The current callstack is collected - // automatically. - TrackException(err interface{}) -} -``` - -These may be used directly to log basic telemetry a manner you might expect: - -```go -client.TrackMetric("Queue Length", len(queue)) - -client.TrackEvent("Client connected") -``` - -But the inputs to these methods only capture the very basics of what these -telemetry types can represent. For example, all telemetry supports custom -properties, which are inaccessible through the above methods. More complete -versions are available through use of *telemetry item* classes, which can -then be submitted through the `TelemetryClient.Track` method, as illustrated -in the below sections: - -### Trace -[Trace telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TraceTelemetry) -represent printf-like trace statements that can be text searched. They have -an associated severity level, values for which are found in the package's -constants: - -```go -const ( - Verbose contracts.SeverityLevel = contracts.Verbose - Information contracts.SeverityLevel = contracts.Information - Warning contracts.SeverityLevel = contracts.Warning - Error contracts.SeverityLevel = contracts.Error - Critical contracts.SeverityLevel = contracts.Critical -) -``` - -Trace telemetry is fairly simple, but common telemetry properties are also -available: - -```go -trace := appinsights.NewTraceTelemetry("message", appinsights.Warning) - -// You can set custom properties on traces -trace.Properties["module"] = "server" - -// You can also fudge the timestamp: -trace.Timestamp = time.Now().Sub(time.Minute) - -// Finally, track it -client.Track(trace) -``` - -### Events -[Event telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#EventTelemetry) -represent structured event records. - -```go -event := appinsights.NewEventTelemetry("button clicked") -event.Properties["property"] = "value" -client.Track(event) -``` - -### Single-value metrics -[Metric telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#MetricTelemetry) -each represent a single data point. - -```go -metric := appinsights.NewMetricTelemetry("Queue length", len(q.items)) -metric.Properties["Queue name"] = q.name -client.Track(metric) -``` - -### Pre-aggregated metrics -To reduce the number of metric values that may be sent through telemetry, -when using a particularly high volume of measurements, metric data can be -[pre-aggregated by the client](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#AggregateMetricTelemetry) -and submitted all at once. - -```go -aggregate := appinsights.NewAggregateMetricTelemetry("metric name") - -var dataPoints []float64 -// ...collect data points... - -// If the data is sampled, then one should use the AddSampledData method to -// feed data to this telemetry type. -aggregate.AddSampledData(dataPoints) - -// If the entire population of data points is known, then one should instead -// use the AddData method. The difference between the two is the manner in -// which the standard deviation is calculated. -aggregate.AddData(dataPoints) - -// Alternatively, you can aggregate the data yourself and supply it to this -// telemetry item: -aggregate.Value = sum(dataPoints) -aggregate.Min = min(dataPoints) -aggregate.Max = max(dataPoints) -aggregate.Count = len(dataPoints) -aggregate.StdDev = stdDev(dataPoints) - -// Custom properties could be further added here... - -// Finally, track it: -client.Track(aggregate) -``` - -### Requests -[Request telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#RequestTelemetry) -represent completion of an external request to the application and contains -a summary of that request execution and results. This SDK's request -telemetry is focused on HTTP requests. - -```go -request := appinsights.NewRequestTelemetry("GET", "https://microsoft.com/", duration, "") - -// Note that the timestamp will be set to time.Now() minus the -// specified duration. This can be overridden by either manually -// setting the Timestamp and Duration fields, or with MarkTime: -request.MarkTime(requestStartTime, requestEndTime) - -// Source of request -request.Source = clientAddress - -// Success is normally inferred from the responseCode, but can be overridden: -request.Success = false - -// Request ID's are randomly generated GUIDs, but this can also be overridden: -request.Id = "" - -// Custom properties and measurements can be set here -request.Properties["user-agent"] = request.headers["User-agent"] -request.Measurements["POST size"] = float64(len(data)) - -// Context tags become more useful here as well -request.Tags.Session().SetId("") -request.Tags.User().SetAccountId("") - -// Finally track it -client.Track(request) -``` - -### Dependencies -[Remote dependency telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#RemoteDependencyTelemetry) -represent interactions of the monitored component with a remote -component/service like SQL or an HTTP endpoint. - -```go -dependency := appinsights.NewRemoteDependencyTelemetry("Redis cache", "Redis", "", true /* success */) - -// The result code is typically an error code or response status code -dependency.ResultCode = "OK" - -// Id's can be used for correlation if the remote end is also logging -// telemetry through application insights. -dependency.Id = "" - -// Data may contain the exact URL hit or SQL statements -dependency.Data = "MGET " - -// The duration can be set directly: -dependency.Duration = time.Minute -// or via MarkTime: -dependency.MarkTime(startTime, endTime) - -// Properties and measurements may be set. -dependency.Properties["shard-instance"] = "" -dependency.Measurements["data received"] = float64(len(response.data)) - -// Submit the telemetry -client.Track(dependency) -``` - -### Exceptions -[Exception telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#ExceptionTelemetry) -represent handled or unhandled exceptions that occurred during the execution -of the monitored application. This SDK is geared towards handling panics or -unexpected results from important functions: - -To handle a panic: - -```go -func method(client appinsights.TelemetryClient) { - defer func() { - if r := recover(); r != nil { - // Track the panic - client.TrackException(r) - - // Optionally, you may want to re-throw the panic: - panic(r) - } - }() - - // Panics in any code below will be handled by the above. - panic("AHHHH!!") -} -``` - -This can be condensed with a helper function: - -```go -func method(client appinsights.TelemetryClient) { - // false indicates that we should have this handle the panic, and - // not re-throw it. - defer appinsights.TrackPanic(client, false) - - // Panics in any code below will be handled by the above. - panic("AHHHH!!") -} -``` - -This will capture and report the call stack of the panic, including the site -of the function that handled the panic. Do note that Go does not unwind the -callstack while processing panics, so the trace will include any functions -that may be called by `method` in the example above leading up to the panic. - -This SDK will handle panic messages that are any of the types: `string`, -`error`, or anything that implements [fmt.Stringer](https://golang.org/pkg/fmt/#Stringer) -or [fmt.GoStringer](https://golang.org/pkg/fmt/#GoStringer). - -While the above example uses `client.TrackException`, you can also use the -longer form as in earlier examples -- and not only for panics: - -```go -value, err := someMethod(argument) -if err != nil { - exception := appinsights.NewExceptionTelemetry(err) - - // Set the severity level -- perhaps this isn't a critical - // issue, but we'd *really rather* it didn't fail: - exception.SeverityLevel = appinsights.Warning - - // One could tweak the number of stack frames to skip by - // reassigning the callstack -- for instance, if you were to - // log this exception in a helper method. - exception.Frames = appinsights.GetCallstack(3 /* frames to skip */) - - // Properties are available as usual - exception.Properties["input"] = argument - - // Track the exception - client.Track(exception) -} -``` - -### Availability -[Availability telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#AvailabilityTelemetry) -represent the result of executing an availability test. This is useful if -you are writing availability monitors in Go. - -```go -availability := appinsights.NewAvailabilityTelemetry("test name", callDuration, true /* success */) - -// The run location indicates where the test was run from -availability.RunLocation = "Phoenix" - -// Diagnostics message -availability.Message = diagnostics - -// Id is used for correlation with the target service -availability.Id = requestId - -// Timestamp and duration can be changed through MarkTime, similar -// to other telemetry types with Duration's -availability.MarkTime(testStartTime, testEndTime) - -// Submit the telemetry -client.Track(availability) -``` - -### Page Views -[Page view telemetry items](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#PageViewTelemetry) -represent generic actions on a page like a button click. These are typically -generated by the client side rather than the server side, but is available -here nonetheless. - -```go -pageview := appinsights.NewPageViewTelemetry("Event name", "http://testuri.org/page") - -// A duration is available here. -pageview.Duration = time.Minute - -// As are the usual Properties and Measurements... - -// Track -client.Track(pageview) -``` - -### Context tags -Telemetry items all have a `Tags` property that contains information *about* -the submitted telemetry, such as user, session, and device information. The -`Tags` property is an instance of the -[contracts.ContextTags](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/contracts/#ContextTags) -type, which is a `map[string]string` under the hood, but has helper methods -to access the most commonly used data. An instance of -[TelemetryContext](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#TelemetryContext) -exists on the `TelemetryClient`, and also contains a `Tags` property. These -tags are applied to all telemetry sent through the client. If a context tag -is found on both the client's `TelemetryContext` and in the telemetry item's -`Tags`, the value associated with the telemtry takes precedence. - -A few examples for illustration: - -```go -import ( - "os" - - "github.com/microsoft/ApplicationInsights-Go/appinsights" - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -func main() { - client := appinsights.NewTelemetryClient("") - - // Set role instance name globally -- this is usually the - // name of the service submitting the telemetry - client.Context().Tags.Cloud().SetRole("my_go_server") - - // Set the role instance to the host name. Note that this is - // done automatically by the SDK. - client.Context().Tags.Cloud().SetRoleInstance(os.Hostname()) - - // Make a request to fiddle with the telemetry's context - req := appinsights.NewRequestTelemetry("GET", "http://server/path", time.Millisecond, "200") - - // Set the account ID context tag, for this telemetry item - // only. The following are equivalent: - req.Tags.User().SetAccountId("") - req.Tags[contracts.UserAccountId] = "" - - // This request will have all context tags above. - client.Track(req) -} -``` - -### Common properties - -In the same way that context tags can be written to all telemetry items, the -`TelemetryContext` has a `CommonProperties` map. Entries in this map will -be added to all telemetry items' custom properties (unless a telemetry item -already has that property set -- the telemetry item always has precedence). -This is useful for contextual data that may not be captured in the context -tags, for instance cluster identifiers or resource groups. - -```go -func main() { - client := appinsights.NewTelemetryClient("") - - client.Context().CommonProperties["Resource group"] = "My resource group" - // ... -} -``` - -### Shutdown -The Go SDK submits data asynchronously. The [InMemoryChannel](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights/#InMemoryChannel) -launches its own goroutine used to accept and send telemetry. If you're not -careful, this may result in lost telemetry when the service needs to shut -down. The channel has a few methods to deal with this case: - -* `Flush` will trigger telemetry submission for buffered items. It returns - immediately and telemetry is not guaranteed to have been sent. -* `Stop` will immediately shut down the channel and discard any unsubmitted - telemetry. Useful if you need to exit NOW. -* `Close` will cause the channel to stop accepting new telemetry, submit any - pending telemetry, and returns a channel that closes when the telemetry - buffer is fully empty. If telemetry submission fails, then `Close` will - retry until the specified duration elapses. If no duration is specified, - then it will give up if any telemetry submission fails. - -If at all possible, you should use `Close`: - -```go -func main() { - client := appinsights.NewTelemetryClient("") - - // ... run the service ... - - // on shutdown: - - select { - case <-client.Channel().Close(10 * time.Second): - // Ten second timeout for retries. - - // If we got here, then all telemetry was submitted - // successfully, and we can proceed to exiting. - case <-time.After(30 * time.Second): - // Thirty second absolute timeout. This covers any - // previous telemetry submission that may not have - // completed before Close was called. - - // There are a number of reasons we could have - // reached here. We gave it a go, but telemetry - // submission failed somewhere. Perhaps old events - // were still retrying, or perhaps we're throttled. - // Either way, we don't want to wait around for it - // to complete, so let's just exit. - } -} -``` - -We recommend something similar to the above to minimize lost telemetry -through shutdown. -[The documentation](https://godoc.org/github.com/microsoft/ApplicationInsights-Go/appinsights#TelemetryChannel) -explains in more detail what can lead to the cases above. - -### Diagnostics -If you find yourself missing some of the telemetry that you thought was -submitted, diagnostics can be turned on to help troubleshoot problems with -telemetry submission. - -```go -appinsights.NewDiagnosticsMessageListener(func(msg string) error { - fmt.Printf("[%s] %s\n", time.Now().Format(time.UnixDate), msg) - return nil -}) - -// go about your business... -``` - -The SDK will emit messages during every telemetry submission. Successful -submissions will look something like this: - -``` -[Tue Nov 21 18:59:41 PST 2017] --------- Transmitting 16 items --------- -[Tue Nov 21 18:59:41 PST 2017] Telemetry transmitted in 708.382896ms -[Tue Nov 21 18:59:41 PST 2017] Response: 200 -``` - -If telemetry is rejected, the errors from the data collector endpoint will -be displayed: - -``` -[Tue Nov 21 18:58:39 PST 2017] --------- Transmitting 16 items --------- -[Tue Nov 21 18:58:40 PST 2017] Telemetry transmitted in 1.034608896s -[Tue Nov 21 18:58:40 PST 2017] Response: 206 -[Tue Nov 21 18:58:40 PST 2017] Items accepted/received: 15/16 -[Tue Nov 21 18:58:40 PST 2017] Errors: -[Tue Nov 21 18:58:40 PST 2017] #9 - 400 109: Field 'name' on type 'RemoteDependencyData' is required but missing or empty. Expected: string, Actual: -[Tue Nov 21 18:58:40 PST 2017] Telemetry item: - {"ver":1,"name":"Microsoft.ApplicationInsights.RemoteDependency","time":"2017-11-22T02:58:39Z","sampleRate":100,"seq":"","iKey":"","tags":{"ai.cloud.roleInstance":"","ai.device.id":"","ai.device.osVersion":"linux","ai.internal.sdkVersion":"go:0.4.0-pre","ai.operation.id":"bf755161-7725-490c-872e-69815826a94c"},"data":{"baseType":"RemoteDependencyData","baseData":{"ver":2,"name":"","id":"","resultCode":"","duration":"0.00:00:00.0000000","success":true,"data":"","target":"http://bing.com","type":"HTTP"}}} - -[Tue Nov 21 18:58:40 PST 2017] Refusing to retry telemetry submission (retry==false) -``` - -Information about retries, server throttling, and more from the SDK's -perspective will also be available. - -Please include this diagnostic information (with ikey's blocked out) when -submitting bug reports to this project. diff --git a/application-insights/SECURITY.md b/application-insights/SECURITY.md deleted file mode 100644 index 869fdfe2b2..0000000000 --- a/application-insights/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). - - diff --git a/application-insights/appinsights/client.go b/application-insights/appinsights/client.go deleted file mode 100644 index d532e03a00..0000000000 --- a/application-insights/appinsights/client.go +++ /dev/null @@ -1,155 +0,0 @@ -package appinsights - -import ( - "time" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -// Application Insights telemetry client provides interface to track telemetry -// items. -type TelemetryClient interface { - // Gets the telemetry context for this client. Values found on this - // context will get written out to every telemetry item tracked by - // this client. - Context() *TelemetryContext - - // Gets the instrumentation key assigned to this telemetry client. - InstrumentationKey() string - - // Gets the telemetry channel used to submit data to the backend. - Channel() TelemetryChannel - - // Gets whether this client is enabled and will accept telemetry. - IsEnabled() bool - - // Enables or disables the telemetry client. When disabled, telemetry - // is silently swallowed by the client. Defaults to enabled. - SetIsEnabled(enabled bool) - - // Submits the specified telemetry item. - Track(telemetry Telemetry) - - // Log a user action with the specified name - TrackEvent(name string) - - // Log a numeric value that is not specified with a specific event. - // Typically used to send regular reports of performance indicators. - TrackMetric(name string, value float64) - - // Log a trace message with the specified severity level. - TrackTrace(name string, severity contracts.SeverityLevel) - - // Log an HTTP request with the specified method, URL, duration and - // response code. - TrackRequest(method, url string, duration time.Duration, responseCode string) - - // Log a dependency with the specified name, type, target, and - // success status. - TrackRemoteDependency(name, dependencyType, target string, success bool) - - // Log an availability test result with the specified test name, - // duration, and success status. - TrackAvailability(name string, duration time.Duration, success bool) - - // Log an exception with the specified error, which may be a string, - // error or Stringer. The current callstack is collected - // automatically. - TrackException(err interface{}) -} - -type telemetryClient struct { - channel TelemetryChannel - context *TelemetryContext - isEnabled bool -} - -// Creates a new telemetry client instance that submits telemetry with the -// specified instrumentation key. -func NewTelemetryClient(iKey string) TelemetryClient { - return NewTelemetryClientFromConfig(NewTelemetryConfiguration(iKey)) -} - -// Creates a new telemetry client instance configured by the specified -// TelemetryConfiguration object. -func NewTelemetryClientFromConfig(config *TelemetryConfiguration) TelemetryClient { - return &telemetryClient{ - channel: NewInMemoryChannel(config), - context: config.setupContext(), - isEnabled: true, - } -} - -// Gets the telemetry context for this client. Values found on this context -// will get written out to every telemetry item tracked by this client. -func (tc *telemetryClient) Context() *TelemetryContext { - return tc.context -} - -// Gets the telemetry channel used to submit data to the backend. -func (tc *telemetryClient) Channel() TelemetryChannel { - return tc.channel -} - -// Gets the instrumentation key assigned to this telemetry client. -func (tc *telemetryClient) InstrumentationKey() string { - return tc.context.InstrumentationKey() -} - -// Gets whether this client is enabled and will accept telemetry. -func (tc *telemetryClient) IsEnabled() bool { - return tc.isEnabled -} - -// Enables or disables the telemetry client. When disabled, telemetry is -// silently swallowed by the client. Defaults to enabled. -func (tc *telemetryClient) SetIsEnabled(isEnabled bool) { - tc.isEnabled = isEnabled -} - -// Submits the specified telemetry item. -func (tc *telemetryClient) Track(item Telemetry) { - if tc.isEnabled && item != nil { - tc.channel.Send(tc.context.envelop(item)) - } -} - -// Log a user action with the specified name -func (tc *telemetryClient) TrackEvent(name string) { - tc.Track(NewEventTelemetry(name)) -} - -// Log a numeric value that is not specified with a specific event. -// Typically used to send regular reports of performance indicators. -func (tc *telemetryClient) TrackMetric(name string, value float64) { - tc.Track(NewMetricTelemetry(name, value)) -} - -// Log a trace message with the specified severity level. -func (tc *telemetryClient) TrackTrace(message string, severity contracts.SeverityLevel) { - tc.Track(NewTraceTelemetry(message, severity)) -} - -// Log an HTTP request with the specified method, URL, duration and response -// code. -func (tc *telemetryClient) TrackRequest(method, url string, duration time.Duration, responseCode string) { - tc.Track(NewRequestTelemetry(method, url, duration, responseCode)) -} - -// Log a dependency with the specified name, type, target, and success -// status. -func (tc *telemetryClient) TrackRemoteDependency(name, dependencyType, target string, success bool) { - tc.Track(NewRemoteDependencyTelemetry(name, dependencyType, target, success)) -} - -// Log an availability test result with the specified test name, duration, -// and success status. -func (tc *telemetryClient) TrackAvailability(name string, duration time.Duration, success bool) { - tc.Track(NewAvailabilityTelemetry(name, duration, success)) -} - -// Log an exception with the specified error, which may be a string, error -// or Stringer. The current callstack is collected automatically. -func (tc *telemetryClient) TrackException(err interface{}) { - tc.Track(newExceptionTelemetry(err, 1)) -} diff --git a/application-insights/appinsights/client_test.go b/application-insights/appinsights/client_test.go deleted file mode 100644 index 740c43c339..0000000000 --- a/application-insights/appinsights/client_test.go +++ /dev/null @@ -1,236 +0,0 @@ -package appinsights - -import ( - "bytes" - "compress/gzip" - "fmt" - "io/ioutil" - "strings" - "testing" - "time" -) - -func BenchmarkClientBurstPerformance(b *testing.B) { - client := NewTelemetryClient("") - client.(*telemetryClient).channel.(*InMemoryChannel).transmitter = &nullTransmitter{} - - for i := 0; i < b.N; i++ { - client.TrackTrace("A message", Information) - } - - <-client.Channel().Close(time.Minute) -} - -func TestClientProperties(t *testing.T) { - client := NewTelemetryClient(test_ikey) - defer client.Channel().Close() - - if _, ok := client.Channel().(*InMemoryChannel); !ok { - t.Error("Client's Channel() is not InMemoryChannel") - } - - if ikey := client.InstrumentationKey(); ikey != test_ikey { - t.Error("Client's InstrumentationKey is not expected") - } - - if ikey := client.Context().InstrumentationKey(); ikey != test_ikey { - t.Error("Context's InstrumentationKey is not expected") - } - - if client.Context() == nil { - t.Error("Client.Context == nil") - } - - if client.IsEnabled() == false { - t.Error("Client.IsEnabled == false") - } - - client.SetIsEnabled(false) - if client.IsEnabled() == true { - t.Error("Client.SetIsEnabled had no effect") - } - - if client.Channel().EndpointAddress() != "https://dc.services.visualstudio.com/v2/track" { - t.Error("Client.Channel.EndpointAddress was incorrect") - } -} - -func TestClientPropertiesWithConnectionString(t *testing.T) { - client := NewTelemetryClientFromConfig(NewTelemetryConfigurationWithConnectionString(connection_string)) - defer client.Channel().Close() - - if _, ok := client.Channel().(*InMemoryChannel); !ok { - t.Error("Client's Channel() is not InMemoryChannel") - } - - if ikey := client.InstrumentationKey(); ikey != "00000000-0000-0000-0000-000000000000" { - t.Error("Client's InstrumentationKey is not expected") - } - - if ikey := client.Context().InstrumentationKey(); ikey != "00000000-0000-0000-0000-000000000000" { - t.Error("Context's InstrumentationKey is not expected") - } - - if client.Context() == nil { - t.Error("Client.Context == nil") - } - - if client.IsEnabled() == false { - t.Error("Client.IsEnabled == false") - } - - client.SetIsEnabled(false) - if client.IsEnabled() == true { - t.Error("Client.SetIsEnabled had no effect") - } - - if client.Channel().EndpointAddress() != "https://ingestion.endpoint.com/v2/track" { - t.Error("Client.Channel.EndpointAddress was incorrect") - } -} - -func TestEndToEnd(t *testing.T) { - mockClock(time.Unix(1511001321, 0)) - defer resetClock() - xmit, server := newTestClientServer() - defer server.Close() - - config := NewTelemetryConfiguration(test_ikey) - config.EndpointUrl = xmit.(*httpTransmitter).endpoint - client := NewTelemetryClientFromConfig(config) - defer client.Channel().Close() - - // Track directly off the client - client.TrackEvent("client-event") - client.TrackMetric("client-metric", 44.0) - client.TrackTrace("client-trace", Information) - client.TrackRequest("GET", "www.testurl.org", time.Minute, "404") - - // NOTE: A lot of this is covered elsewhere, so we won't duplicate - // *too* much. - - // Set up server response - server.responseData = []byte(`{"itemsReceived":4, "itemsAccepted":4, "errors":[]}`) - server.responseHeaders["Content-type"] = "application/json" - - // Wait for automatic transmit -- get the request - slowTick(11) - req := server.waitForRequest(t) - - // GZIP magic number - if len(req.body) < 2 || req.body[0] != 0x1f || req.body[1] != 0x8b { - t.Fatal("Missing gzip magic number") - } - - // Decompress - reader, err := gzip.NewReader(bytes.NewReader(req.body)) - if err != nil { - t.Fatalf("Coudln't create gzip reader: %s", err.Error()) - } - - // Read payload - body, err := ioutil.ReadAll(reader) - reader.Close() - if err != nil { - t.Fatalf("Couldn't read compressed data: %s", err.Error()) - } - - // Check out payload - j, err := parsePayload(body) - if err != nil { - t.Errorf("Error parsing payload: %s", err.Error()) - } - - if len(j) != 4 { - t.Fatal("Unexpected event count") - } - - j[0].assertPath(t, "iKey", test_ikey) - j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") - j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") - - j[1].assertPath(t, "iKey", test_ikey) - j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") - j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") - - j[2].assertPath(t, "iKey", test_ikey) - j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") - j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") - - j[3].assertPath(t, "iKey", test_ikey) - j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") - j[3].assertPath(t, "time", "2017-11-18T10:34:21Z") -} - -func TestEndToEndWithConnectionString(t *testing.T) { - mockClock(time.Unix(1511001321, 0)) - defer resetClock() - xmit, server := newTestClientServer() - defer server.Close() - - baseEndpoint := strings.TrimSuffix(xmit.(*httpTransmitter).endpoint, "v2/track") - testConnectionString := fmt.Sprintf("InstrumentationKey=%s;IngestionEndpoint=%s", test_ikey, baseEndpoint) - client := NewTelemetryClientFromConfig(NewTelemetryConfigurationWithConnectionString(testConnectionString)) - defer client.Channel().Close() - - // Track directly off the client - client.TrackEvent("client-event") - client.TrackMetric("client-metric", 44.0) - client.TrackTrace("client-trace", Information) - client.TrackRequest("GET", "www.testurl.org", time.Minute, "404") - - // NOTE: A lot of this is covered elsewhere, so we won't duplicate - // *too* much. - - // Set up server response - server.responseData = []byte(`{"itemsReceived":4, "itemsAccepted":4, "errors":[]}`) - server.responseHeaders["Content-type"] = "application/json" - - // Wait for automatic transmit -- get the request - slowTick(11) - req := server.waitForRequest(t) - - // GZIP magic number - if len(req.body) < 2 || req.body[0] != 0x1f || req.body[1] != 0x8b { - t.Fatal("Missing gzip magic number") - } - - // Decompress - reader, err := gzip.NewReader(bytes.NewReader(req.body)) - if err != nil { - t.Fatalf("Coudln't create gzip reader: %s", err.Error()) - } - - // Read payload - body, err := ioutil.ReadAll(reader) - reader.Close() - if err != nil { - t.Fatalf("Couldn't read compressed data: %s", err.Error()) - } - - // Check out payload - j, err := parsePayload(body) - if err != nil { - t.Errorf("Error parsing payload: %s", err.Error()) - } - - if len(j) != 4 { - t.Fatal("Unexpected event count") - } - - j[0].assertPath(t, "iKey", test_ikey) - j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") - j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") - - j[1].assertPath(t, "iKey", test_ikey) - j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") - j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") - - j[2].assertPath(t, "iKey", test_ikey) - j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") - j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") - - j[3].assertPath(t, "iKey", test_ikey) - j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") - j[3].assertPath(t, "time", "2017-11-18T10:34:21Z") -} diff --git a/application-insights/appinsights/clock.go b/application-insights/appinsights/clock.go deleted file mode 100644 index 1178b9eaa7..0000000000 --- a/application-insights/appinsights/clock.go +++ /dev/null @@ -1,11 +0,0 @@ -package appinsights - -// We need to mock out the clock for tests; we'll use this to do it. - -import "code.cloudfoundry.org/clock" - -var currentClock clock.Clock - -func init() { - currentClock = clock.NewClock() -} diff --git a/application-insights/appinsights/clock_test.go b/application-insights/appinsights/clock_test.go deleted file mode 100644 index 8da85b6f9a..0000000000 --- a/application-insights/appinsights/clock_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package appinsights - -import ( - "time" - - "code.cloudfoundry.org/clock" - "code.cloudfoundry.org/clock/fakeclock" -) - -var fakeClock *fakeclock.FakeClock - -func mockClock(timestamp ...time.Time) { - if len(timestamp) > 0 { - fakeClock = fakeclock.NewFakeClock(timestamp[0]) - } else { - fakeClock = fakeclock.NewFakeClock(time.Now().Round(time.Minute)) - } - - currentClock = fakeClock -} - -func resetClock() { - fakeClock = nil - currentClock = clock.NewClock() -} - -func slowTick(seconds int) { - const delay = time.Millisecond * time.Duration(5) - - // Sleeps in tests are evil, but with all the async nonsense going - // on, no callbacks, and minimal control of the clock, I'm not - // really sure I have another choice. - - time.Sleep(delay) - for i := 0; i < seconds; i++ { - fakeClock.Increment(time.Second) - time.Sleep(delay) - } -} diff --git a/application-insights/appinsights/configuration.go b/application-insights/appinsights/configuration.go deleted file mode 100644 index c21fa9f1f1..0000000000 --- a/application-insights/appinsights/configuration.go +++ /dev/null @@ -1,68 +0,0 @@ -package appinsights - -import ( - "net/http" - "os" - "runtime" - "time" -) - -// Configuration data used to initialize a new TelemetryClient. -type TelemetryConfiguration struct { - // Instrumentation key for the client. - InstrumentationKey string - - // Endpoint URL where data will be submitted. - EndpointUrl string - - // Maximum number of telemetry items that can be submitted in each - // request. If this many items are buffered, the buffer will be - // flushed before MaxBatchInterval expires. - MaxBatchSize int - - // Maximum time to wait before sending a batch of telemetry. - MaxBatchInterval time.Duration - - // Customized http client if desired (will use http.DefaultClient otherwise) - Client *http.Client -} - -// Creates a new TelemetryConfiguration object with the specified -// instrumentation key and default values. -func NewTelemetryConfiguration(instrumentationKey string) *TelemetryConfiguration { - return &TelemetryConfiguration{ - InstrumentationKey: instrumentationKey, - EndpointUrl: "https://dc.services.visualstudio.com/v2/track", - MaxBatchSize: 1024, - MaxBatchInterval: time.Duration(10) * time.Second, - } -} - -// Creates a new TelemetryConfiguration object with the specified -// connection string and endpoint URL. -func NewTelemetryConfigurationWithConnectionString(connectionString string) *TelemetryConfiguration { - connectionParams, err := parseConnectionString(connectionString) - if err != nil { - return nil - } - - return &TelemetryConfiguration{ - InstrumentationKey: connectionParams.InstrumentationKey, - EndpointUrl: connectionParams.IngestionEndpoint, - MaxBatchSize: 1024, - MaxBatchInterval: time.Duration(10) * time.Second, - } -} - -func (config *TelemetryConfiguration) setupContext() *TelemetryContext { - context := NewTelemetryContext(config.InstrumentationKey) - context.Tags.Internal().SetSdkVersion(sdkName + ":" + Version) - context.Tags.Device().SetOsVersion(runtime.GOOS) - - if hostname, err := os.Hostname(); err == nil { - context.Tags.Device().SetId(hostname) - context.Tags.Cloud().SetRoleInstance(hostname) - } - - return context -} diff --git a/application-insights/appinsights/configuration_test.go b/application-insights/appinsights/configuration_test.go deleted file mode 100644 index 8404092a72..0000000000 --- a/application-insights/appinsights/configuration_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package appinsights - -import "testing" - -func TestTelemetryConfiguration(t *testing.T) { - testKey := "test" - defaultEndpoint := "https://dc.services.visualstudio.com/v2/track" - - config := NewTelemetryConfiguration(testKey) - - if config.InstrumentationKey != testKey { - t.Errorf("InstrumentationKey is %s, want %s", config.InstrumentationKey, testKey) - } - - if config.EndpointUrl != defaultEndpoint { - t.Errorf("EndpointUrl is %s, want %s", config.EndpointUrl, defaultEndpoint) - } - - if config.Client != nil { - t.Errorf("Client is not nil, want nil") - } -} - -func TestTelemetryConfigurationWithConnectionString(t *testing.T) { - config := NewTelemetryConfigurationWithConnectionString(connection_string) - - if config.InstrumentationKey != "00000000-0000-0000-0000-000000000000" { - t.Errorf("InstrumentationKey is %s, want 00000000-0000-0000-0000-000000000000", config.InstrumentationKey) - } - - if config.EndpointUrl != "https://ingestion.endpoint.com/v2/track" { - t.Errorf("EndpointUrl is %s, want https://ingestion.endpoint.com/v2/track", config.EndpointUrl) - } - - if config.Client != nil { - t.Errorf("Client is not nil, want nil") - } -} diff --git a/application-insights/appinsights/connection_string_parser.go b/application-insights/appinsights/connection_string_parser.go deleted file mode 100644 index 04e963ec2b..0000000000 --- a/application-insights/appinsights/connection_string_parser.go +++ /dev/null @@ -1,39 +0,0 @@ -package appinsights - -import ( - "errors" - "strings" -) - -type ConnectionParams struct { - InstrumentationKey string - IngestionEndpoint string -} - -func parseConnectionString(cString string) (*ConnectionParams, error) { - connectionParams := &ConnectionParams{} - - pairs := strings.Split(cString, ";") - for _, pair := range pairs { - kv := strings.SplitN(pair, "=", 2) - if len(kv) != 2 { - return nil, errors.New("invalid connection parameter format") - } - - key, value := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]) - - switch strings.ToLower(key) { - case "instrumentationkey": - connectionParams.InstrumentationKey = value - case "ingestionendpoint": - if value != "" { - connectionParams.IngestionEndpoint = value + "v2/track" - } - } - } - - if connectionParams.InstrumentationKey == "" || connectionParams.IngestionEndpoint == "" { - return nil, errors.New("missing required connection parameters") - } - return connectionParams, nil -} diff --git a/application-insights/appinsights/connection_string_parser_test.go b/application-insights/appinsights/connection_string_parser_test.go deleted file mode 100644 index 87cb718f25..0000000000 --- a/application-insights/appinsights/connection_string_parser_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package appinsights - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -const connection_string = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" - -func TestConnectionStringParser(t *testing.T) { - tests := []struct { - name string - cString string - want *ConnectionParams - wantErr bool - }{ - { - name: "Valid connection string and instrumentation key", - cString: connection_string, - want: &ConnectionParams{ - InstrumentationKey: "00000000-0000-0000-0000-000000000000", - IngestionEndpoint: "https://ingestion.endpoint.com/v2/track", - }, - wantErr: false, - }, - { - name: "Invalid connection string format", - cString: "Invalid connection string", - want: nil, - wantErr: true, - }, - { - name: "Valid instrumentation key with missing ingestion endpoint", - cString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=", - want: nil, - wantErr: true, - }, - { - name: "Missing instrumentation key with valid ingestion endpoint", - cString: "InstrumentationKey=;IngestionEndpoint=https://ingestion.endpoint.com/v2/track", - want: nil, - wantErr: true, - }, - { - name: "Empty connection string", - cString: "", - want: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseConnectionString(tt.cString) - if tt.wantErr { - require.Error(t, err, "Expected an error but got none") - } else { - require.NoError(t, err, "Expected no error but got one") - require.NotNil(t, got, "Expected a non-nil result") - require.Equal(t, tt.want.InstrumentationKey, got.InstrumentationKey, "Instrumentation Key does not match") - require.Equal(t, tt.want.IngestionEndpoint, got.IngestionEndpoint, "Ingestion Endpoint does not match") - } - }) - } -} diff --git a/application-insights/appinsights/constants.go b/application-insights/appinsights/constants.go deleted file mode 100644 index 060ed59d4e..0000000000 --- a/application-insights/appinsights/constants.go +++ /dev/null @@ -1,20 +0,0 @@ -package appinsights - -// NOTE: This file was automatically generated. - -import "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" - -// Type of the metric data measurement. -const ( - Measurement contracts.DataPointType = contracts.Measurement - Aggregation contracts.DataPointType = contracts.Aggregation -) - -// Defines the level of severity for the event. -const ( - Verbose contracts.SeverityLevel = contracts.Verbose - Information contracts.SeverityLevel = contracts.Information - Warning contracts.SeverityLevel = contracts.Warning - Error contracts.SeverityLevel = contracts.Error - Critical contracts.SeverityLevel = contracts.Critical -) diff --git a/application-insights/appinsights/contracts/availabilitydata.go b/application-insights/appinsights/contracts/availabilitydata.go deleted file mode 100644 index 4f0d709f5c..0000000000 --- a/application-insights/appinsights/contracts/availabilitydata.go +++ /dev/null @@ -1,111 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Instances of AvailabilityData represent the result of executing an -// availability test. -type AvailabilityData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // Identifier of a test run. Use it to correlate steps of test run and - // telemetry generated by the service. - Id string `json:"id"` - - // Name of the test that these availability results represent. - Name string `json:"name"` - - // Duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 days. - Duration string `json:"duration"` - - // Success flag. - Success bool `json:"success"` - - // Name of the location where the test was run from. - RunLocation string `json:"runLocation"` - - // Diagnostic message for the result. - Message string `json:"message"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` - - // Collection of custom measurements. - Measurements map[string]float64 `json:"measurements,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *AvailabilityData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".Availability" - } else { - return "Microsoft.ApplicationInsights.Availability" - } -} - -// Returns the base type when placed within a Data object container. -func (data *AvailabilityData) BaseType() string { - return "AvailabilityData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *AvailabilityData) Sanitize() []string { - var warnings []string - - if len(data.Id) > 64 { - data.Id = data.Id[:64] - warnings = append(warnings, "AvailabilityData.Id exceeded maximum length of 64") - } - - if len(data.Name) > 1024 { - data.Name = data.Name[:1024] - warnings = append(warnings, "AvailabilityData.Name exceeded maximum length of 1024") - } - - if len(data.RunLocation) > 1024 { - data.RunLocation = data.RunLocation[:1024] - warnings = append(warnings, "AvailabilityData.RunLocation exceeded maximum length of 1024") - } - - if len(data.Message) > 8192 { - data.Message = data.Message[:8192] - warnings = append(warnings, "AvailabilityData.Message exceeded maximum length of 8192") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "AvailabilityData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "AvailabilityData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - if data.Measurements != nil { - for k, v := range data.Measurements { - if len(k) > 150 { - data.Measurements[k[:150]] = v - delete(data.Measurements, k) - warnings = append(warnings, "AvailabilityData.Measurements has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new AvailabilityData instance with default values set by the schema. -func NewAvailabilityData() *AvailabilityData { - return &AvailabilityData{ - Ver: 2, - } -} diff --git a/application-insights/appinsights/contracts/base.go b/application-insights/appinsights/contracts/base.go deleted file mode 100644 index 3ceb5022f2..0000000000 --- a/application-insights/appinsights/contracts/base.go +++ /dev/null @@ -1,25 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Data struct to contain only C section with custom fields. -type Base struct { - - // Name of item (B section) if any. If telemetry data is derived straight from - // this, this should be null. - BaseType string `json:"baseType"` -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *Base) Sanitize() []string { - var warnings []string - - return warnings -} - -// Creates a new Base instance with default values set by the schema. -func NewBase() *Base { - return &Base{} -} diff --git a/application-insights/appinsights/contracts/contexttagkeys.go b/application-insights/appinsights/contracts/contexttagkeys.go deleted file mode 100644 index eaf57abb31..0000000000 --- a/application-insights/appinsights/contracts/contexttagkeys.go +++ /dev/null @@ -1,153 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -import "strconv" - -const ( - // Application version. Information in the application context fields is - // always about the application that is sending the telemetry. - ApplicationVersion string = "ai.application.ver" - - // Unique client device id. Computer name in most cases. - DeviceId string = "ai.device.id" - - // Device locale using - pattern, following RFC 5646. - // Example 'en-US'. - DeviceLocale string = "ai.device.locale" - - // Model of the device the end user of the application is using. Used for - // client scenarios. If this field is empty then it is derived from the user - // agent. - DeviceModel string = "ai.device.model" - - // Client device OEM name taken from the browser. - DeviceOEMName string = "ai.device.oemName" - - // Operating system name and version of the device the end user of the - // application is using. If this field is empty then it is derived from the - // user agent. Example 'Windows 10 Pro 10.0.10586.0' - DeviceOSVersion string = "ai.device.osVersion" - - // The type of the device the end user of the application is using. Used - // primarily to distinguish JavaScript telemetry from server side telemetry. - // Examples: 'PC', 'Phone', 'Browser'. 'PC' is the default value. - DeviceType string = "ai.device.type" - - // The IP address of the client device. IPv4 and IPv6 are supported. - // Information in the location context fields is always about the end user. - // When telemetry is sent from a service, the location context is about the - // user that initiated the operation in the service. - LocationIp string = "ai.location.ip" - - // A unique identifier for the operation instance. The operation.id is created - // by either a request or a page view. All other telemetry sets this to the - // value for the containing request or page view. Operation.id is used for - // finding all the telemetry items for a specific operation instance. - OperationId string = "ai.operation.id" - - // The name (group) of the operation. The operation.name is created by either - // a request or a page view. All other telemetry items set this to the value - // for the containing request or page view. Operation.name is used for finding - // all the telemetry items for a group of operations (i.e. 'GET Home/Index'). - OperationName string = "ai.operation.name" - - // The unique identifier of the telemetry item's immediate parent. - OperationParentId string = "ai.operation.parentId" - - // Name of synthetic source. Some telemetry from the application may represent - // a synthetic traffic. It may be web crawler indexing the web site, site - // availability tests or traces from diagnostic libraries like Application - // Insights SDK itself. - OperationSyntheticSource string = "ai.operation.syntheticSource" - - // The correlation vector is a light weight vector clock which can be used to - // identify and order related events across clients and services. - OperationCorrelationVector string = "ai.operation.correlationVector" - - // Session ID - the instance of the user's interaction with the app. - // Information in the session context fields is always about the end user. - // When telemetry is sent from a service, the session context is about the - // user that initiated the operation in the service. - SessionId string = "ai.session.id" - - // Boolean value indicating whether the session identified by ai.session.id is - // first for the user or not. - SessionIsFirst string = "ai.session.isFirst" - - // In multi-tenant applications this is the account ID or name which the user - // is acting with. Examples may be subscription ID for Azure portal or blog - // name blogging platform. - UserAccountId string = "ai.user.accountId" - - // Anonymous user id. Represents the end user of the application. When - // telemetry is sent from a service, the user context is about the user that - // initiated the operation in the service. - UserId string = "ai.user.id" - - // Authenticated user id. The opposite of ai.user.id, this represents the user - // with a friendly name. Since it's PII information it is not collected by - // default by most SDKs. - UserAuthUserId string = "ai.user.authUserId" - - // Name of the role the application is a part of. Maps directly to the role - // name in azure. - CloudRole string = "ai.cloud.role" - - // Name of the instance where the application is running. Computer name for - // on-premisis, instance name for Azure. - CloudRoleInstance string = "ai.cloud.roleInstance" - - // SDK version. See - // https://github.com/microsoft/ApplicationInsights-Home/blob/master/SDK-AUTHORING.md#sdk-version-specification - // for information. - InternalSdkVersion string = "ai.internal.sdkVersion" - - // Agent version. Used to indicate the version of StatusMonitor installed on - // the computer if it is used for data collection. - InternalAgentVersion string = "ai.internal.agentVersion" - - // This is the node name used for billing purposes. Use it to override the - // standard detection of nodes. - InternalNodeName string = "ai.internal.nodeName" -) - -var tagMaxLengths = map[string]int{ - "ai.application.ver": 1024, - "ai.device.id": 1024, - "ai.device.locale": 64, - "ai.device.model": 256, - "ai.device.oemName": 256, - "ai.device.osVersion": 256, - "ai.device.type": 64, - "ai.location.ip": 46, - "ai.operation.id": 128, - "ai.operation.name": 1024, - "ai.operation.parentId": 128, - "ai.operation.syntheticSource": 1024, - "ai.operation.correlationVector": 64, - "ai.session.id": 64, - "ai.session.isFirst": 5, - "ai.user.accountId": 1024, - "ai.user.id": 128, - "ai.user.authUserId": 1024, - "ai.cloud.role": 256, - "ai.cloud.roleInstance": 256, - "ai.internal.sdkVersion": 64, - "ai.internal.agentVersion": 64, - "ai.internal.nodeName": 256, -} - -// Truncates tag values that exceed their maximum supported lengths. Returns -// warnings for each affected field. -func SanitizeTags(tags map[string]string) []string { - var warnings []string - for k, v := range tags { - if maxlen, ok := tagMaxLengths[k]; ok && len(v) > maxlen { - tags[k] = v[:maxlen] - warnings = append(warnings, "Value for "+k+" exceeded maximum length of "+strconv.Itoa(maxlen)) - } - } - - return warnings -} diff --git a/application-insights/appinsights/contracts/contexttags.go b/application-insights/appinsights/contracts/contexttags.go deleted file mode 100644 index 426378318b..0000000000 --- a/application-insights/appinsights/contracts/contexttags.go +++ /dev/null @@ -1,565 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -type ContextTags map[string]string - -// Helper type that provides access to context fields grouped under 'application'. -// This is returned by TelemetryContext.Tags.Application() -type ApplicationContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'device'. -// This is returned by TelemetryContext.Tags.Device() -type DeviceContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'location'. -// This is returned by TelemetryContext.Tags.Location() -type LocationContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'operation'. -// This is returned by TelemetryContext.Tags.Operation() -type OperationContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'session'. -// This is returned by TelemetryContext.Tags.Session() -type SessionContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'user'. -// This is returned by TelemetryContext.Tags.User() -type UserContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'cloud'. -// This is returned by TelemetryContext.Tags.Cloud() -type CloudContextTags ContextTags - -// Helper type that provides access to context fields grouped under 'internal'. -// This is returned by TelemetryContext.Tags.Internal() -type InternalContextTags ContextTags - -// Returns a helper to access context fields grouped under 'application'. -func (tags ContextTags) Application() ApplicationContextTags { - return ApplicationContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'device'. -func (tags ContextTags) Device() DeviceContextTags { - return DeviceContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'location'. -func (tags ContextTags) Location() LocationContextTags { - return LocationContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'operation'. -func (tags ContextTags) Operation() OperationContextTags { - return OperationContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'session'. -func (tags ContextTags) Session() SessionContextTags { - return SessionContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'user'. -func (tags ContextTags) User() UserContextTags { - return UserContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'cloud'. -func (tags ContextTags) Cloud() CloudContextTags { - return CloudContextTags(tags) -} - -// Returns a helper to access context fields grouped under 'internal'. -func (tags ContextTags) Internal() InternalContextTags { - return InternalContextTags(tags) -} - -// Application version. Information in the application context fields is -// always about the application that is sending the telemetry. -func (tags ApplicationContextTags) GetVer() string { - if result, ok := tags["ai.application.ver"]; ok { - return result - } - - return "" -} - -// Application version. Information in the application context fields is -// always about the application that is sending the telemetry. -func (tags ApplicationContextTags) SetVer(value string) { - if value != "" { - tags["ai.application.ver"] = value - } else { - delete(tags, "ai.application.ver") - } -} - -// Unique client device id. Computer name in most cases. -func (tags DeviceContextTags) GetId() string { - if result, ok := tags["ai.device.id"]; ok { - return result - } - - return "" -} - -// Unique client device id. Computer name in most cases. -func (tags DeviceContextTags) SetId(value string) { - if value != "" { - tags["ai.device.id"] = value - } else { - delete(tags, "ai.device.id") - } -} - -// Device locale using - pattern, following RFC 5646. -// Example 'en-US'. -func (tags DeviceContextTags) GetLocale() string { - if result, ok := tags["ai.device.locale"]; ok { - return result - } - - return "" -} - -// Device locale using - pattern, following RFC 5646. -// Example 'en-US'. -func (tags DeviceContextTags) SetLocale(value string) { - if value != "" { - tags["ai.device.locale"] = value - } else { - delete(tags, "ai.device.locale") - } -} - -// Model of the device the end user of the application is using. Used for -// client scenarios. If this field is empty then it is derived from the user -// agent. -func (tags DeviceContextTags) GetModel() string { - if result, ok := tags["ai.device.model"]; ok { - return result - } - - return "" -} - -// Model of the device the end user of the application is using. Used for -// client scenarios. If this field is empty then it is derived from the user -// agent. -func (tags DeviceContextTags) SetModel(value string) { - if value != "" { - tags["ai.device.model"] = value - } else { - delete(tags, "ai.device.model") - } -} - -// Client device OEM name taken from the browser. -func (tags DeviceContextTags) GetOemName() string { - if result, ok := tags["ai.device.oemName"]; ok { - return result - } - - return "" -} - -// Client device OEM name taken from the browser. -func (tags DeviceContextTags) SetOemName(value string) { - if value != "" { - tags["ai.device.oemName"] = value - } else { - delete(tags, "ai.device.oemName") - } -} - -// Operating system name and version of the device the end user of the -// application is using. If this field is empty then it is derived from the -// user agent. Example 'Windows 10 Pro 10.0.10586.0' -func (tags DeviceContextTags) GetOsVersion() string { - if result, ok := tags["ai.device.osVersion"]; ok { - return result - } - - return "" -} - -// Operating system name and version of the device the end user of the -// application is using. If this field is empty then it is derived from the -// user agent. Example 'Windows 10 Pro 10.0.10586.0' -func (tags DeviceContextTags) SetOsVersion(value string) { - if value != "" { - tags["ai.device.osVersion"] = value - } else { - delete(tags, "ai.device.osVersion") - } -} - -// The type of the device the end user of the application is using. Used -// primarily to distinguish JavaScript telemetry from server side telemetry. -// Examples: 'PC', 'Phone', 'Browser'. 'PC' is the default value. -func (tags DeviceContextTags) GetType() string { - if result, ok := tags["ai.device.type"]; ok { - return result - } - - return "" -} - -// The type of the device the end user of the application is using. Used -// primarily to distinguish JavaScript telemetry from server side telemetry. -// Examples: 'PC', 'Phone', 'Browser'. 'PC' is the default value. -func (tags DeviceContextTags) SetType(value string) { - if value != "" { - tags["ai.device.type"] = value - } else { - delete(tags, "ai.device.type") - } -} - -// The IP address of the client device. IPv4 and IPv6 are supported. -// Information in the location context fields is always about the end user. -// When telemetry is sent from a service, the location context is about the -// user that initiated the operation in the service. -func (tags LocationContextTags) GetIp() string { - if result, ok := tags["ai.location.ip"]; ok { - return result - } - - return "" -} - -// The IP address of the client device. IPv4 and IPv6 are supported. -// Information in the location context fields is always about the end user. -// When telemetry is sent from a service, the location context is about the -// user that initiated the operation in the service. -func (tags LocationContextTags) SetIp(value string) { - if value != "" { - tags["ai.location.ip"] = value - } else { - delete(tags, "ai.location.ip") - } -} - -// A unique identifier for the operation instance. The operation.id is created -// by either a request or a page view. All other telemetry sets this to the -// value for the containing request or page view. Operation.id is used for -// finding all the telemetry items for a specific operation instance. -func (tags OperationContextTags) GetId() string { - if result, ok := tags["ai.operation.id"]; ok { - return result - } - - return "" -} - -// A unique identifier for the operation instance. The operation.id is created -// by either a request or a page view. All other telemetry sets this to the -// value for the containing request or page view. Operation.id is used for -// finding all the telemetry items for a specific operation instance. -func (tags OperationContextTags) SetId(value string) { - if value != "" { - tags["ai.operation.id"] = value - } else { - delete(tags, "ai.operation.id") - } -} - -// The name (group) of the operation. The operation.name is created by either -// a request or a page view. All other telemetry items set this to the value -// for the containing request or page view. Operation.name is used for finding -// all the telemetry items for a group of operations (i.e. 'GET Home/Index'). -func (tags OperationContextTags) GetName() string { - if result, ok := tags["ai.operation.name"]; ok { - return result - } - - return "" -} - -// The name (group) of the operation. The operation.name is created by either -// a request or a page view. All other telemetry items set this to the value -// for the containing request or page view. Operation.name is used for finding -// all the telemetry items for a group of operations (i.e. 'GET Home/Index'). -func (tags OperationContextTags) SetName(value string) { - if value != "" { - tags["ai.operation.name"] = value - } else { - delete(tags, "ai.operation.name") - } -} - -// The unique identifier of the telemetry item's immediate parent. -func (tags OperationContextTags) GetParentId() string { - if result, ok := tags["ai.operation.parentId"]; ok { - return result - } - - return "" -} - -// The unique identifier of the telemetry item's immediate parent. -func (tags OperationContextTags) SetParentId(value string) { - if value != "" { - tags["ai.operation.parentId"] = value - } else { - delete(tags, "ai.operation.parentId") - } -} - -// Name of synthetic source. Some telemetry from the application may represent -// a synthetic traffic. It may be web crawler indexing the web site, site -// availability tests or traces from diagnostic libraries like Application -// Insights SDK itself. -func (tags OperationContextTags) GetSyntheticSource() string { - if result, ok := tags["ai.operation.syntheticSource"]; ok { - return result - } - - return "" -} - -// Name of synthetic source. Some telemetry from the application may represent -// a synthetic traffic. It may be web crawler indexing the web site, site -// availability tests or traces from diagnostic libraries like Application -// Insights SDK itself. -func (tags OperationContextTags) SetSyntheticSource(value string) { - if value != "" { - tags["ai.operation.syntheticSource"] = value - } else { - delete(tags, "ai.operation.syntheticSource") - } -} - -// The correlation vector is a light weight vector clock which can be used to -// identify and order related events across clients and services. -func (tags OperationContextTags) GetCorrelationVector() string { - if result, ok := tags["ai.operation.correlationVector"]; ok { - return result - } - - return "" -} - -// The correlation vector is a light weight vector clock which can be used to -// identify and order related events across clients and services. -func (tags OperationContextTags) SetCorrelationVector(value string) { - if value != "" { - tags["ai.operation.correlationVector"] = value - } else { - delete(tags, "ai.operation.correlationVector") - } -} - -// Session ID - the instance of the user's interaction with the app. -// Information in the session context fields is always about the end user. -// When telemetry is sent from a service, the session context is about the -// user that initiated the operation in the service. -func (tags SessionContextTags) GetId() string { - if result, ok := tags["ai.session.id"]; ok { - return result - } - - return "" -} - -// Session ID - the instance of the user's interaction with the app. -// Information in the session context fields is always about the end user. -// When telemetry is sent from a service, the session context is about the -// user that initiated the operation in the service. -func (tags SessionContextTags) SetId(value string) { - if value != "" { - tags["ai.session.id"] = value - } else { - delete(tags, "ai.session.id") - } -} - -// Boolean value indicating whether the session identified by ai.session.id is -// first for the user or not. -func (tags SessionContextTags) GetIsFirst() string { - if result, ok := tags["ai.session.isFirst"]; ok { - return result - } - - return "" -} - -// Boolean value indicating whether the session identified by ai.session.id is -// first for the user or not. -func (tags SessionContextTags) SetIsFirst(value string) { - if value != "" { - tags["ai.session.isFirst"] = value - } else { - delete(tags, "ai.session.isFirst") - } -} - -// In multi-tenant applications this is the account ID or name which the user -// is acting with. Examples may be subscription ID for Azure portal or blog -// name blogging platform. -func (tags UserContextTags) GetAccountId() string { - if result, ok := tags["ai.user.accountId"]; ok { - return result - } - - return "" -} - -// In multi-tenant applications this is the account ID or name which the user -// is acting with. Examples may be subscription ID for Azure portal or blog -// name blogging platform. -func (tags UserContextTags) SetAccountId(value string) { - if value != "" { - tags["ai.user.accountId"] = value - } else { - delete(tags, "ai.user.accountId") - } -} - -// Anonymous user id. Represents the end user of the application. When -// telemetry is sent from a service, the user context is about the user that -// initiated the operation in the service. -func (tags UserContextTags) GetId() string { - if result, ok := tags["ai.user.id"]; ok { - return result - } - - return "" -} - -// Anonymous user id. Represents the end user of the application. When -// telemetry is sent from a service, the user context is about the user that -// initiated the operation in the service. -func (tags UserContextTags) SetId(value string) { - if value != "" { - tags["ai.user.id"] = value - } else { - delete(tags, "ai.user.id") - } -} - -// Authenticated user id. The opposite of ai.user.id, this represents the user -// with a friendly name. Since it's PII information it is not collected by -// default by most SDKs. -func (tags UserContextTags) GetAuthUserId() string { - if result, ok := tags["ai.user.authUserId"]; ok { - return result - } - - return "" -} - -// Authenticated user id. The opposite of ai.user.id, this represents the user -// with a friendly name. Since it's PII information it is not collected by -// default by most SDKs. -func (tags UserContextTags) SetAuthUserId(value string) { - if value != "" { - tags["ai.user.authUserId"] = value - } else { - delete(tags, "ai.user.authUserId") - } -} - -// Name of the role the application is a part of. Maps directly to the role -// name in azure. -func (tags CloudContextTags) GetRole() string { - if result, ok := tags["ai.cloud.role"]; ok { - return result - } - - return "" -} - -// Name of the role the application is a part of. Maps directly to the role -// name in azure. -func (tags CloudContextTags) SetRole(value string) { - if value != "" { - tags["ai.cloud.role"] = value - } else { - delete(tags, "ai.cloud.role") - } -} - -// Name of the instance where the application is running. Computer name for -// on-premisis, instance name for Azure. -func (tags CloudContextTags) GetRoleInstance() string { - if result, ok := tags["ai.cloud.roleInstance"]; ok { - return result - } - - return "" -} - -// Name of the instance where the application is running. Computer name for -// on-premisis, instance name for Azure. -func (tags CloudContextTags) SetRoleInstance(value string) { - if value != "" { - tags["ai.cloud.roleInstance"] = value - } else { - delete(tags, "ai.cloud.roleInstance") - } -} - -// SDK version. See -// https://github.com/microsoft/ApplicationInsights-Home/blob/master/SDK-AUTHORING.md#sdk-version-specification -// for information. -func (tags InternalContextTags) GetSdkVersion() string { - if result, ok := tags["ai.internal.sdkVersion"]; ok { - return result - } - - return "" -} - -// SDK version. See -// https://github.com/microsoft/ApplicationInsights-Home/blob/master/SDK-AUTHORING.md#sdk-version-specification -// for information. -func (tags InternalContextTags) SetSdkVersion(value string) { - if value != "" { - tags["ai.internal.sdkVersion"] = value - } else { - delete(tags, "ai.internal.sdkVersion") - } -} - -// Agent version. Used to indicate the version of StatusMonitor installed on -// the computer if it is used for data collection. -func (tags InternalContextTags) GetAgentVersion() string { - if result, ok := tags["ai.internal.agentVersion"]; ok { - return result - } - - return "" -} - -// Agent version. Used to indicate the version of StatusMonitor installed on -// the computer if it is used for data collection. -func (tags InternalContextTags) SetAgentVersion(value string) { - if value != "" { - tags["ai.internal.agentVersion"] = value - } else { - delete(tags, "ai.internal.agentVersion") - } -} - -// This is the node name used for billing purposes. Use it to override the -// standard detection of nodes. -func (tags InternalContextTags) GetNodeName() string { - if result, ok := tags["ai.internal.nodeName"]; ok { - return result - } - - return "" -} - -// This is the node name used for billing purposes. Use it to override the -// standard detection of nodes. -func (tags InternalContextTags) SetNodeName(value string) { - if value != "" { - tags["ai.internal.nodeName"] = value - } else { - delete(tags, "ai.internal.nodeName") - } -} diff --git a/application-insights/appinsights/contracts/data.go b/application-insights/appinsights/contracts/data.go deleted file mode 100644 index 144b7a8e50..0000000000 --- a/application-insights/appinsights/contracts/data.go +++ /dev/null @@ -1,25 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Data struct to contain both B and C sections. -type Data struct { - Base - - // Container for data item (B section). - BaseData interface{} `json:"baseData"` -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *Data) Sanitize() []string { - var warnings []string - - return warnings -} - -// Creates a new Data instance with default values set by the schema. -func NewData() *Data { - return &Data{} -} diff --git a/application-insights/appinsights/contracts/datapoint.go b/application-insights/appinsights/contracts/datapoint.go deleted file mode 100644 index b06beb1e5c..0000000000 --- a/application-insights/appinsights/contracts/datapoint.go +++ /dev/null @@ -1,54 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Metric data single measurement. -type DataPoint struct { - - // Name of the metric. - Name string `json:"name"` - - // Metric type. Single measurement or the aggregated value. - Kind DataPointType `json:"kind"` - - // Single value for measurement. Sum of individual measurements for the - // aggregation. - Value float64 `json:"value"` - - // Metric weight of the aggregated metric. Should not be set for a - // measurement. - Count int `json:"count"` - - // Minimum value of the aggregated metric. Should not be set for a - // measurement. - Min float64 `json:"min"` - - // Maximum value of the aggregated metric. Should not be set for a - // measurement. - Max float64 `json:"max"` - - // Standard deviation of the aggregated metric. Should not be set for a - // measurement. - StdDev float64 `json:"stdDev"` -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *DataPoint) Sanitize() []string { - var warnings []string - - if len(data.Name) > 1024 { - data.Name = data.Name[:1024] - warnings = append(warnings, "DataPoint.Name exceeded maximum length of 1024") - } - - return warnings -} - -// Creates a new DataPoint instance with default values set by the schema. -func NewDataPoint() *DataPoint { - return &DataPoint{ - Kind: Measurement, - } -} diff --git a/application-insights/appinsights/contracts/datapointtype.go b/application-insights/appinsights/contracts/datapointtype.go deleted file mode 100644 index 8f468e7a3c..0000000000 --- a/application-insights/appinsights/contracts/datapointtype.go +++ /dev/null @@ -1,22 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Type of the metric data measurement. -type DataPointType int - -const ( - Measurement DataPointType = 0 - Aggregation DataPointType = 1 -) - -func (value DataPointType) String() string { - switch int(value) { - case 0: - return "Measurement" - case 1: - return "Aggregation" - default: - return "" - } -} diff --git a/application-insights/appinsights/contracts/domain.go b/application-insights/appinsights/contracts/domain.go deleted file mode 100644 index 024945baec..0000000000 --- a/application-insights/appinsights/contracts/domain.go +++ /dev/null @@ -1,21 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// The abstract common base of all domains. -type Domain struct { -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *Domain) Sanitize() []string { - var warnings []string - - return warnings -} - -// Creates a new Domain instance with default values set by the schema. -func NewDomain() *Domain { - return &Domain{} -} diff --git a/application-insights/appinsights/contracts/envelope.go b/application-insights/appinsights/contracts/envelope.go deleted file mode 100644 index 91c80a9d5d..0000000000 --- a/application-insights/appinsights/contracts/envelope.go +++ /dev/null @@ -1,82 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// System variables for a telemetry item. -type Envelope struct { - - // Envelope version. For internal use only. By assigning this the default, it - // will not be serialized within the payload unless changed to a value other - // than #1. - Ver int `json:"ver"` - - // Type name of telemetry data item. - Name string `json:"name"` - - // Event date time when telemetry item was created. This is the wall clock - // time on the client when the event was generated. There is no guarantee that - // the client's time is accurate. This field must be formatted in UTC ISO 8601 - // format, with a trailing 'Z' character, as described publicly on - // https://en.wikipedia.org/wiki/ISO_8601#UTC. Note: the number of decimal - // seconds digits provided are variable (and unspecified). Consumers should - // handle this, i.e. managed code consumers should not use format 'O' for - // parsing as it specifies a fixed length. Example: - // 2009-06-15T13:45:30.0000000Z. - Time string `json:"time"` - - // Sampling rate used in application. This telemetry item represents 1 / - // sampleRate actual telemetry items. - SampleRate float64 `json:"sampleRate"` - - // Sequence field used to track absolute order of uploaded events. - Seq string `json:"seq"` - - // The application's instrumentation key. The key is typically represented as - // a GUID, but there are cases when it is not a guid. No code should rely on - // iKey being a GUID. Instrumentation key is case insensitive. - IKey string `json:"iKey"` - - // Key/value collection of context properties. See ContextTagKeys for - // information on available properties. - Tags map[string]string `json:"tags,omitempty"` - - // Telemetry data item. - Data interface{} `json:"data"` -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *Envelope) Sanitize() []string { - var warnings []string - - if len(data.Name) > 1024 { - data.Name = data.Name[:1024] - warnings = append(warnings, "Envelope.Name exceeded maximum length of 1024") - } - - if len(data.Time) > 64 { - data.Time = data.Time[:64] - warnings = append(warnings, "Envelope.Time exceeded maximum length of 64") - } - - if len(data.Seq) > 64 { - data.Seq = data.Seq[:64] - warnings = append(warnings, "Envelope.Seq exceeded maximum length of 64") - } - - if len(data.IKey) > 40 { - data.IKey = data.IKey[:40] - warnings = append(warnings, "Envelope.IKey exceeded maximum length of 40") - } - - return warnings -} - -// Creates a new Envelope instance with default values set by the schema. -func NewEnvelope() *Envelope { - return &Envelope{ - Ver: 1, - SampleRate: 100.0, - } -} diff --git a/application-insights/appinsights/contracts/eventdata.go b/application-insights/appinsights/contracts/eventdata.go deleted file mode 100644 index 2093c74fd0..0000000000 --- a/application-insights/appinsights/contracts/eventdata.go +++ /dev/null @@ -1,82 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Instances of Event represent structured event records that can be grouped -// and searched by their properties. Event data item also creates a metric of -// event count by name. -type EventData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // Event name. Keep it low cardinality to allow proper grouping and useful - // metrics. - Name string `json:"name"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` - - // Collection of custom measurements. - Measurements map[string]float64 `json:"measurements,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *EventData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".Event" - } else { - return "Microsoft.ApplicationInsights.Event" - } -} - -// Returns the base type when placed within a Data object container. -func (data *EventData) BaseType() string { - return "EventData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *EventData) Sanitize() []string { - var warnings []string - - if len(data.Name) > 512 { - data.Name = data.Name[:512] - warnings = append(warnings, "EventData.Name exceeded maximum length of 512") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "EventData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "EventData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - if data.Measurements != nil { - for k, v := range data.Measurements { - if len(k) > 150 { - data.Measurements[k[:150]] = v - delete(data.Measurements, k) - warnings = append(warnings, "EventData.Measurements has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new EventData instance with default values set by the schema. -func NewEventData() *EventData { - return &EventData{ - Ver: 2, - } -} diff --git a/application-insights/appinsights/contracts/exceptiondata.go b/application-insights/appinsights/contracts/exceptiondata.go deleted file mode 100644 index fe1c2f2b8e..0000000000 --- a/application-insights/appinsights/contracts/exceptiondata.go +++ /dev/null @@ -1,93 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// An instance of Exception represents a handled or unhandled exception that -// occurred during execution of the monitored application. -type ExceptionData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // Exception chain - list of inner exceptions. - Exceptions []*ExceptionDetails `json:"exceptions"` - - // Severity level. Mostly used to indicate exception severity level when it is - // reported by logging library. - SeverityLevel SeverityLevel `json:"severityLevel"` - - // Identifier of where the exception was thrown in code. Used for exceptions - // grouping. Typically a combination of exception type and a function from the - // call stack. - ProblemId string `json:"problemId"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` - - // Collection of custom measurements. - Measurements map[string]float64 `json:"measurements,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *ExceptionData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".Exception" - } else { - return "Microsoft.ApplicationInsights.Exception" - } -} - -// Returns the base type when placed within a Data object container. -func (data *ExceptionData) BaseType() string { - return "ExceptionData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *ExceptionData) Sanitize() []string { - var warnings []string - - for _, ptr := range data.Exceptions { - warnings = append(warnings, ptr.Sanitize()...) - } - - if len(data.ProblemId) > 1024 { - data.ProblemId = data.ProblemId[:1024] - warnings = append(warnings, "ExceptionData.ProblemId exceeded maximum length of 1024") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "ExceptionData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "ExceptionData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - if data.Measurements != nil { - for k, v := range data.Measurements { - if len(k) > 150 { - data.Measurements[k[:150]] = v - delete(data.Measurements, k) - warnings = append(warnings, "ExceptionData.Measurements has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new ExceptionData instance with default values set by the schema. -func NewExceptionData() *ExceptionData { - return &ExceptionData{ - Ver: 2, - } -} diff --git a/application-insights/appinsights/contracts/exceptiondetails.go b/application-insights/appinsights/contracts/exceptiondetails.go deleted file mode 100644 index 8b768ab6cf..0000000000 --- a/application-insights/appinsights/contracts/exceptiondetails.go +++ /dev/null @@ -1,66 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Exception details of the exception in a chain. -type ExceptionDetails struct { - - // In case exception is nested (outer exception contains inner one), the id - // and outerId properties are used to represent the nesting. - Id int `json:"id"` - - // The value of outerId is a reference to an element in ExceptionDetails that - // represents the outer exception - OuterId int `json:"outerId"` - - // Exception type name. - TypeName string `json:"typeName"` - - // Exception message. - Message string `json:"message"` - - // Indicates if full exception stack is provided in the exception. The stack - // may be trimmed, such as in the case of a StackOverflow exception. - HasFullStack bool `json:"hasFullStack"` - - // Text describing the stack. Either stack or parsedStack should have a value. - Stack string `json:"stack"` - - // List of stack frames. Either stack or parsedStack should have a value. - ParsedStack []*StackFrame `json:"parsedStack,omitempty"` -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *ExceptionDetails) Sanitize() []string { - var warnings []string - - if len(data.TypeName) > 1024 { - data.TypeName = data.TypeName[:1024] - warnings = append(warnings, "ExceptionDetails.TypeName exceeded maximum length of 1024") - } - - if len(data.Message) > 32768 { - data.Message = data.Message[:32768] - warnings = append(warnings, "ExceptionDetails.Message exceeded maximum length of 32768") - } - - if len(data.Stack) > 32768 { - data.Stack = data.Stack[:32768] - warnings = append(warnings, "ExceptionDetails.Stack exceeded maximum length of 32768") - } - - for _, ptr := range data.ParsedStack { - warnings = append(warnings, ptr.Sanitize()...) - } - - return warnings -} - -// Creates a new ExceptionDetails instance with default values set by the schema. -func NewExceptionDetails() *ExceptionDetails { - return &ExceptionDetails{ - HasFullStack: true, - } -} diff --git a/application-insights/appinsights/contracts/messagedata.go b/application-insights/appinsights/contracts/messagedata.go deleted file mode 100644 index c0676431f2..0000000000 --- a/application-insights/appinsights/contracts/messagedata.go +++ /dev/null @@ -1,72 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Instances of Message represent printf-like trace statements that are -// text-searched. Log4Net, NLog and other text-based log file entries are -// translated into intances of this type. The message does not have -// measurements. -type MessageData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // Trace message - Message string `json:"message"` - - // Trace severity level. - SeverityLevel SeverityLevel `json:"severityLevel"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *MessageData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".Message" - } else { - return "Microsoft.ApplicationInsights.Message" - } -} - -// Returns the base type when placed within a Data object container. -func (data *MessageData) BaseType() string { - return "MessageData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *MessageData) Sanitize() []string { - var warnings []string - - if len(data.Message) > 32768 { - data.Message = data.Message[:32768] - warnings = append(warnings, "MessageData.Message exceeded maximum length of 32768") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "MessageData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "MessageData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new MessageData instance with default values set by the schema. -func NewMessageData() *MessageData { - return &MessageData{ - Ver: 2, - } -} diff --git a/application-insights/appinsights/contracts/metricdata.go b/application-insights/appinsights/contracts/metricdata.go deleted file mode 100644 index 106576f2c7..0000000000 --- a/application-insights/appinsights/contracts/metricdata.go +++ /dev/null @@ -1,68 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// An instance of the Metric item is a list of measurements (single data -// points) and/or aggregations. -type MetricData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // List of metrics. Only one metric in the list is currently supported by - // Application Insights storage. If multiple data points were sent only the - // first one will be used. - Metrics []*DataPoint `json:"metrics"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *MetricData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".Metric" - } else { - return "Microsoft.ApplicationInsights.Metric" - } -} - -// Returns the base type when placed within a Data object container. -func (data *MetricData) BaseType() string { - return "MetricData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *MetricData) Sanitize() []string { - var warnings []string - - for _, ptr := range data.Metrics { - warnings = append(warnings, ptr.Sanitize()...) - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "MetricData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "MetricData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new MetricData instance with default values set by the schema. -func NewMetricData() *MetricData { - return &MetricData{ - Ver: 2, - } -} diff --git a/application-insights/appinsights/contracts/package.go b/application-insights/appinsights/contracts/package.go deleted file mode 100644 index ac96d6d35e..0000000000 --- a/application-insights/appinsights/contracts/package.go +++ /dev/null @@ -1,4 +0,0 @@ -// Data contract definitions for telemetry submitted to Application Insights. -// This is generated from the schemas found at -// https://github.com/microsoft/ApplicationInsights-Home/tree/master/EndpointSpecs/Schemas/Bond -package contracts diff --git a/application-insights/appinsights/contracts/pageviewdata.go b/application-insights/appinsights/contracts/pageviewdata.go deleted file mode 100644 index 15e1d0aa93..0000000000 --- a/application-insights/appinsights/contracts/pageviewdata.go +++ /dev/null @@ -1,85 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// An instance of PageView represents a generic action on a page like a button -// click. It is also the base type for PageView. -type PageViewData struct { - Domain - EventData - - // Request URL with all query string parameters - Url string `json:"url"` - - // Request duration in format: DD.HH:MM:SS.MMMMMM. For a page view - // (PageViewData), this is the duration. For a page view with performance - // information (PageViewPerfData), this is the page load time. Must be less - // than 1000 days. - Duration string `json:"duration"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *PageViewData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".PageView" - } else { - return "Microsoft.ApplicationInsights.PageView" - } -} - -// Returns the base type when placed within a Data object container. -func (data *PageViewData) BaseType() string { - return "PageViewData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *PageViewData) Sanitize() []string { - var warnings []string - - if len(data.Url) > 2048 { - data.Url = data.Url[:2048] - warnings = append(warnings, "PageViewData.Url exceeded maximum length of 2048") - } - - if len(data.Name) > 512 { - data.Name = data.Name[:512] - warnings = append(warnings, "PageViewData.Name exceeded maximum length of 512") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "PageViewData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "PageViewData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - if data.Measurements != nil { - for k, v := range data.Measurements { - if len(k) > 150 { - data.Measurements[k[:150]] = v - delete(data.Measurements, k) - warnings = append(warnings, "PageViewData.Measurements has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new PageViewData instance with default values set by the schema. -func NewPageViewData() *PageViewData { - return &PageViewData{ - EventData: EventData{ - Ver: 2, - }, - } -} diff --git a/application-insights/appinsights/contracts/remotedependencydata.go b/application-insights/appinsights/contracts/remotedependencydata.go deleted file mode 100644 index f078243f45..0000000000 --- a/application-insights/appinsights/contracts/remotedependencydata.go +++ /dev/null @@ -1,134 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// An instance of Remote Dependency represents an interaction of the monitored -// component with a remote component/service like SQL or an HTTP endpoint. -type RemoteDependencyData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // Name of the command initiated with this dependency call. Low cardinality - // value. Examples are stored procedure name and URL path template. - Name string `json:"name"` - - // Identifier of a dependency call instance. Used for correlation with the - // request telemetry item corresponding to this dependency call. - Id string `json:"id"` - - // Result code of a dependency call. Examples are SQL error code and HTTP - // status code. - ResultCode string `json:"resultCode"` - - // Request duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 - // days. - Duration string `json:"duration"` - - // Indication of successfull or unsuccessfull call. - Success bool `json:"success"` - - // Command initiated by this dependency call. Examples are SQL statement and - // HTTP URL's with all query parameters. - Data string `json:"data"` - - // Target site of a dependency call. Examples are server name, host address. - Target string `json:"target"` - - // Dependency type name. Very low cardinality value for logical grouping of - // dependencies and interpretation of other fields like commandName and - // resultCode. Examples are SQL, Azure table, and HTTP. - Type string `json:"type"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` - - // Collection of custom measurements. - Measurements map[string]float64 `json:"measurements,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *RemoteDependencyData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".RemoteDependency" - } else { - return "Microsoft.ApplicationInsights.RemoteDependency" - } -} - -// Returns the base type when placed within a Data object container. -func (data *RemoteDependencyData) BaseType() string { - return "RemoteDependencyData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *RemoteDependencyData) Sanitize() []string { - var warnings []string - - if len(data.Name) > 1024 { - data.Name = data.Name[:1024] - warnings = append(warnings, "RemoteDependencyData.Name exceeded maximum length of 1024") - } - - if len(data.Id) > 128 { - data.Id = data.Id[:128] - warnings = append(warnings, "RemoteDependencyData.Id exceeded maximum length of 128") - } - - if len(data.ResultCode) > 1024 { - data.ResultCode = data.ResultCode[:1024] - warnings = append(warnings, "RemoteDependencyData.ResultCode exceeded maximum length of 1024") - } - - if len(data.Data) > 8192 { - data.Data = data.Data[:8192] - warnings = append(warnings, "RemoteDependencyData.Data exceeded maximum length of 8192") - } - - if len(data.Target) > 1024 { - data.Target = data.Target[:1024] - warnings = append(warnings, "RemoteDependencyData.Target exceeded maximum length of 1024") - } - - if len(data.Type) > 1024 { - data.Type = data.Type[:1024] - warnings = append(warnings, "RemoteDependencyData.Type exceeded maximum length of 1024") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "RemoteDependencyData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "RemoteDependencyData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - if data.Measurements != nil { - for k, v := range data.Measurements { - if len(k) > 150 { - data.Measurements[k[:150]] = v - delete(data.Measurements, k) - warnings = append(warnings, "RemoteDependencyData.Measurements has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new RemoteDependencyData instance with default values set by the schema. -func NewRemoteDependencyData() *RemoteDependencyData { - return &RemoteDependencyData{ - Ver: 2, - Success: true, - } -} diff --git a/application-insights/appinsights/contracts/requestdata.go b/application-insights/appinsights/contracts/requestdata.go deleted file mode 100644 index 7db3b0aa90..0000000000 --- a/application-insights/appinsights/contracts/requestdata.go +++ /dev/null @@ -1,125 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// An instance of Request represents completion of an external request to the -// application to do work and contains a summary of that request execution and -// the results. -type RequestData struct { - Domain - - // Schema version - Ver int `json:"ver"` - - // Identifier of a request call instance. Used for correlation between request - // and other telemetry items. - Id string `json:"id"` - - // Source of the request. Examples are the instrumentation key of the caller - // or the ip address of the caller. - Source string `json:"source"` - - // Name of the request. Represents code path taken to process request. Low - // cardinality value to allow better grouping of requests. For HTTP requests - // it represents the HTTP method and URL path template like 'GET - // /values/{id}'. - Name string `json:"name"` - - // Request duration in format: DD.HH:MM:SS.MMMMMM. Must be less than 1000 - // days. - Duration string `json:"duration"` - - // Result of a request execution. HTTP status code for HTTP requests. - ResponseCode string `json:"responseCode"` - - // Indication of successfull or unsuccessfull call. - Success bool `json:"success"` - - // Request URL with all query string parameters. - Url string `json:"url"` - - // Collection of custom properties. - Properties map[string]string `json:"properties,omitempty"` - - // Collection of custom measurements. - Measurements map[string]float64 `json:"measurements,omitempty"` -} - -// Returns the name used when this is embedded within an Envelope container. -func (data *RequestData) EnvelopeName(key string) string { - if key != "" { - return "Microsoft.ApplicationInsights." + key + ".Request" - } else { - return "Microsoft.ApplicationInsights.Request" - } -} - -// Returns the base type when placed within a Data object container. -func (data *RequestData) BaseType() string { - return "RequestData" -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *RequestData) Sanitize() []string { - var warnings []string - - if len(data.Id) > 128 { - data.Id = data.Id[:128] - warnings = append(warnings, "RequestData.Id exceeded maximum length of 128") - } - - if len(data.Source) > 1024 { - data.Source = data.Source[:1024] - warnings = append(warnings, "RequestData.Source exceeded maximum length of 1024") - } - - if len(data.Name) > 1024 { - data.Name = data.Name[:1024] - warnings = append(warnings, "RequestData.Name exceeded maximum length of 1024") - } - - if len(data.ResponseCode) > 1024 { - data.ResponseCode = data.ResponseCode[:1024] - warnings = append(warnings, "RequestData.ResponseCode exceeded maximum length of 1024") - } - - if len(data.Url) > 2048 { - data.Url = data.Url[:2048] - warnings = append(warnings, "RequestData.Url exceeded maximum length of 2048") - } - - if data.Properties != nil { - for k, v := range data.Properties { - if len(v) > 8192 { - data.Properties[k] = v[:8192] - warnings = append(warnings, "RequestData.Properties has value with length exceeding max of 8192: "+k) - } - if len(k) > 150 { - data.Properties[k[:150]] = data.Properties[k] - delete(data.Properties, k) - warnings = append(warnings, "RequestData.Properties has key with length exceeding max of 150: "+k) - } - } - } - - if data.Measurements != nil { - for k, v := range data.Measurements { - if len(k) > 150 { - data.Measurements[k[:150]] = v - delete(data.Measurements, k) - warnings = append(warnings, "RequestData.Measurements has key with length exceeding max of 150: "+k) - } - } - } - - return warnings -} - -// Creates a new RequestData instance with default values set by the schema. -func NewRequestData() *RequestData { - return &RequestData{ - Ver: 2, - } -} diff --git a/application-insights/appinsights/contracts/severitylevel.go b/application-insights/appinsights/contracts/severitylevel.go deleted file mode 100644 index a2ec9b8f03..0000000000 --- a/application-insights/appinsights/contracts/severitylevel.go +++ /dev/null @@ -1,31 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Defines the level of severity for the event. -type SeverityLevel int - -const ( - Verbose SeverityLevel = 0 - Information SeverityLevel = 1 - Warning SeverityLevel = 2 - Error SeverityLevel = 3 - Critical SeverityLevel = 4 -) - -func (value SeverityLevel) String() string { - switch int(value) { - case 0: - return "Verbose" - case 1: - return "Information" - case 2: - return "Warning" - case 3: - return "Error" - case 4: - return "Critical" - default: - return "" - } -} diff --git a/application-insights/appinsights/contracts/stackframe.go b/application-insights/appinsights/contracts/stackframe.go deleted file mode 100644 index d012f6b140..0000000000 --- a/application-insights/appinsights/contracts/stackframe.go +++ /dev/null @@ -1,52 +0,0 @@ -package contracts - -// NOTE: This file was automatically generated. - -// Stack frame information. -type StackFrame struct { - - // Level in the call stack. For the long stacks SDK may not report every - // function in a call stack. - Level int `json:"level"` - - // Method name. - Method string `json:"method"` - - // Name of the assembly (dll, jar, etc.) containing this function. - Assembly string `json:"assembly"` - - // File name or URL of the method implementation. - FileName string `json:"fileName"` - - // Line number of the code implementation. - Line int `json:"line"` -} - -// Truncates string fields that exceed their maximum supported sizes for this -// object and all objects it references. Returns a warning for each affected -// field. -func (data *StackFrame) Sanitize() []string { - var warnings []string - - if len(data.Method) > 1024 { - data.Method = data.Method[:1024] - warnings = append(warnings, "StackFrame.Method exceeded maximum length of 1024") - } - - if len(data.Assembly) > 1024 { - data.Assembly = data.Assembly[:1024] - warnings = append(warnings, "StackFrame.Assembly exceeded maximum length of 1024") - } - - if len(data.FileName) > 1024 { - data.FileName = data.FileName[:1024] - warnings = append(warnings, "StackFrame.FileName exceeded maximum length of 1024") - } - - return warnings -} - -// Creates a new StackFrame instance with default values set by the schema. -func NewStackFrame() *StackFrame { - return &StackFrame{} -} diff --git a/application-insights/appinsights/diagnostics.go b/application-insights/appinsights/diagnostics.go deleted file mode 100644 index 7ff90bfaee..0000000000 --- a/application-insights/appinsights/diagnostics.go +++ /dev/null @@ -1,88 +0,0 @@ -package appinsights - -import ( - "fmt" - "sync" -) - -type diagnosticsMessageWriter struct { - listeners []*diagnosticsMessageListener - lock sync.Mutex -} - -// Handler function for receiving diagnostics messages. If this returns an -// error, then the listener will be removed. -type DiagnosticsMessageHandler func(string) error - -// Listener type returned by NewDiagnosticsMessageListener. -type DiagnosticsMessageListener interface { - // Stop receiving diagnostics messages from this listener. - Remove() -} - -type diagnosticsMessageListener struct { - handler DiagnosticsMessageHandler - writer *diagnosticsMessageWriter -} - -func (listener *diagnosticsMessageListener) Remove() { - listener.writer.removeListener(listener) -} - -// The one and only diagnostics writer. -var diagnosticsWriter = &diagnosticsMessageWriter{} - -// Subscribes the specified handler to diagnostics messages from the SDK. The -// returned interface can be used to unsubscribe. -func NewDiagnosticsMessageListener(handler DiagnosticsMessageHandler) DiagnosticsMessageListener { - listener := &diagnosticsMessageListener{ - handler: handler, - writer: diagnosticsWriter, - } - - diagnosticsWriter.appendListener(listener) - return listener -} - -func (writer *diagnosticsMessageWriter) appendListener(listener *diagnosticsMessageListener) { - writer.lock.Lock() - defer writer.lock.Unlock() - writer.listeners = append(writer.listeners, listener) -} - -func (writer *diagnosticsMessageWriter) removeListener(listener *diagnosticsMessageListener) { - writer.lock.Lock() - defer writer.lock.Unlock() - - for i := 0; i < len(writer.listeners); i++ { - if writer.listeners[i] == listener { - writer.listeners[i] = writer.listeners[len(writer.listeners)-1] - writer.listeners = writer.listeners[:len(writer.listeners)-1] - return - } - } -} - -func (writer *diagnosticsMessageWriter) Write(message string) { - var toRemove []*diagnosticsMessageListener - for _, listener := range writer.listeners { - if err := listener.handler(message); err != nil { - toRemove = append(toRemove, listener) - } - } - - for _, listener := range toRemove { - listener.Remove() - } -} - -func (writer *diagnosticsMessageWriter) Printf(message string, args ...interface{}) { - // Don't bother with Sprintf if nobody is listening - if writer.hasListeners() { - writer.Write(fmt.Sprintf(message, args...)) - } -} - -func (writer *diagnosticsMessageWriter) hasListeners() bool { - return len(writer.listeners) > 0 -} diff --git a/application-insights/appinsights/diagnostics_test.go b/application-insights/appinsights/diagnostics_test.go deleted file mode 100644 index d5f741d51c..0000000000 --- a/application-insights/appinsights/diagnostics_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package appinsights - -import ( - "fmt" - "testing" - "time" -) - -func TestMessageSentToConsumers(t *testing.T) { - original := "~~~test_message~~~" - - // There may be spurious messages sent by a transmitter's goroutine from another test, - // so just check that we do get the test message *at some point*. - - listener1chan := make(chan bool, 1) - NewDiagnosticsMessageListener(func(message string) error { - if message == original { - listener1chan <- true - } - - return nil - }) - - listener2chan := make(chan bool, 1) - NewDiagnosticsMessageListener(func(message string) error { - if message == original { - listener2chan <- true - } - - return nil - }) - - defer resetDiagnosticsListeners() - diagnosticsWriter.Write(original) - - listener1recvd := false - listener2recvd := false - timeout := false - timer := time.After(time.Second) - for !(listener1recvd && listener2recvd) && !timeout { - select { - case <-listener1chan: - listener1recvd = true - case <-listener2chan: - listener2recvd = true - case <-timer: - timeout = true - } - } - - if timeout { - t.Errorf("Message failed to be delivered to both listeners") - } -} - -func TestRemoveListener(t *testing.T) { - mchan := make(chan string, 1) - listener := NewDiagnosticsMessageListener(func(message string) error { - mchan <- message - return nil - }) - - defer resetDiagnosticsListeners() - - diagnosticsWriter.Write("Hello") - select { - case <-mchan: - default: - t.Fatalf("Message not received") - } - - listener.Remove() - - diagnosticsWriter.Write("Hello") - select { - case <-mchan: - t.Fatalf("Message received after remove") - default: - } -} - -func TestErroredListenerIsRemoved(t *testing.T) { - mchan := make(chan string, 1) - echan := make(chan error, 1) - NewDiagnosticsMessageListener(func(message string) error { - mchan <- message - return <-echan - }) - defer resetDiagnosticsListeners() - - echan <- nil - diagnosticsWriter.Write("Hello") - select { - case <-mchan: - default: - t.Fatal("Message not received") - } - - echan <- fmt.Errorf("Test error") - diagnosticsWriter.Write("Hello") - select { - case <-mchan: - default: - t.Fatal("Message not received") - } - - echan <- nil - diagnosticsWriter.Write("Not received") - select { - case <-mchan: - t.Fatalf("Message received after error") - default: - } -} - -func resetDiagnosticsListeners() { - diagnosticsWriter.lock.Lock() - defer diagnosticsWriter.lock.Unlock() - diagnosticsWriter.listeners = diagnosticsWriter.listeners[:0] -} diff --git a/application-insights/appinsights/exception.go b/application-insights/appinsights/exception.go deleted file mode 100644 index c440797dcf..0000000000 --- a/application-insights/appinsights/exception.go +++ /dev/null @@ -1,150 +0,0 @@ -package appinsights - -import ( - "fmt" - "reflect" - "runtime" - "strings" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -// Exception telemetry items represent a handled or unhandled exceptions that -// occurred during execution of the monitored application. -type ExceptionTelemetry struct { - BaseTelemetry - BaseTelemetryMeasurements - - // Panic message: string, error, or Stringer - Error interface{} - - // List of stack frames. Use GetCallstack to generate this data. - Frames []*contracts.StackFrame - - // Severity level. - SeverityLevel contracts.SeverityLevel -} - -// Creates a new exception telemetry item with the specified error and the -// current callstack. This should be used directly from a function that -// handles a recover(), or to report an unexpected error return value from -// a function. -func NewExceptionTelemetry(err interface{}) *ExceptionTelemetry { - return newExceptionTelemetry(err, 1) -} - -func newExceptionTelemetry(err interface{}, skip int) *ExceptionTelemetry { - return &ExceptionTelemetry{ - Error: err, - Frames: GetCallstack(2 + skip), - SeverityLevel: Error, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - BaseTelemetryMeasurements: BaseTelemetryMeasurements{ - Measurements: make(map[string]float64), - }, - } -} - -func (telem *ExceptionTelemetry) TelemetryData() TelemetryData { - details := contracts.NewExceptionDetails() - details.HasFullStack = len(telem.Frames) > 0 - details.ParsedStack = telem.Frames - - if err, ok := telem.Error.(error); ok { - details.Message = err.Error() - details.TypeName = reflect.TypeOf(telem.Error).String() - } else if str, ok := telem.Error.(string); ok { - details.Message = str - details.TypeName = "string" - } else if stringer, ok := telem.Error.(fmt.Stringer); ok { - details.Message = stringer.String() - details.TypeName = reflect.TypeOf(telem.Error).String() - } else if stringer, ok := telem.Error.(fmt.GoStringer); ok { - details.Message = stringer.GoString() - details.TypeName = reflect.TypeOf(telem.Error).String() - } else { - details.Message = "" - details.TypeName = "" - } - - data := contracts.NewExceptionData() - data.SeverityLevel = telem.SeverityLevel - data.Exceptions = []*contracts.ExceptionDetails{details} - data.Properties = telem.Properties - data.Measurements = telem.Measurements - - return data -} - -// Generates a callstack suitable for inclusion in Application Insights -// exception telemetry for the current goroutine, skipping a number of frames -// specified by skip. -func GetCallstack(skip int) []*contracts.StackFrame { - var stackFrames []*contracts.StackFrame - - if skip < 0 { - skip = 0 - } - - stack := make([]uintptr, 64+skip) - depth := runtime.Callers(skip+1, stack) - if depth == 0 { - return stackFrames - } - - frames := runtime.CallersFrames(stack[:depth]) - level := 0 - for { - frame, more := frames.Next() - - stackFrame := &contracts.StackFrame{ - Level: level, - FileName: frame.File, - Line: frame.Line, - } - - if frame.Function != "" { - /* Default */ - stackFrame.Method = frame.Function - - /* Break up function into assembly/function */ - lastSlash := strings.LastIndexByte(frame.Function, '/') - if lastSlash < 0 { - // e.g. "runtime.gopanic" - // The below works with lastSlash=0 - lastSlash = 0 - } - - firstDot := strings.IndexByte(frame.Function[lastSlash:], '.') - if firstDot >= 0 { - stackFrame.Assembly = frame.Function[:lastSlash+firstDot] - stackFrame.Method = frame.Function[lastSlash+firstDot+1:] - } - } - - stackFrames = append(stackFrames, stackFrame) - - level++ - if !more { - break - } - } - - return stackFrames -} - -// Recovers from any active panics and tracks them to the specified -// TelemetryClient. If rethrow is set to true, then this will panic. -// Should be invoked via defer in functions to monitor. -func TrackPanic(client TelemetryClient, rethrow bool) { - if r := recover(); r != nil { - client.Track(newExceptionTelemetry(r, 1)) - if rethrow { - panic(r) - } - } -} diff --git a/application-insights/appinsights/exception_test.go b/application-insights/appinsights/exception_test.go deleted file mode 100644 index c42b91d1f9..0000000000 --- a/application-insights/appinsights/exception_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package appinsights - -import ( - "fmt" - "strings" - "testing" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -type myStringer struct{} - -func (s *myStringer) String() string { - return "My stringer error" -} - -type myError struct{} - -func (s *myError) Error() string { - return "My error error" -} - -type myGoStringer struct{} - -func (s *myGoStringer) Error() string { - return "My go stringer error" -} - -func TestExceptionTelemetry(t *testing.T) { - // Test callstack capture -- these should all fit in 64 frames. - for i := 9; i < 20; i++ { - exd := testExceptionCallstack(t, i) - checkDataContract(t, "ExceptionDetails.TypeName", exd.TypeName, "string") - checkDataContract(t, "ExceptionDetails.Message", exd.Message, "Whoops") - checkDataContract(t, "ExceptionDetails.HasFullStack", exd.HasFullStack, true) - } - - // Test error types - var err error - err = &myError{} - - e1 := catchPanic(err) - exd1 := e1.TelemetryData().(*contracts.ExceptionData).Exceptions[0] - checkDataContract(t, "ExceptionDetails.Message", exd1.Message, "My error error") - checkDataContract(t, "ExceptionDetails.TypeName", exd1.TypeName, "*appinsights.myError") - - e2 := catchPanic(&myStringer{}) - exd2 := e2.TelemetryData().(*contracts.ExceptionData).Exceptions[0] - checkDataContract(t, "ExceptionDetails.Message", exd2.Message, "My stringer error") - checkDataContract(t, "ExceptionDetails.TypeName", exd2.TypeName, "*appinsights.myStringer") - - e3 := catchPanic(&myGoStringer{}) - exd3 := e3.TelemetryData().(*contracts.ExceptionData).Exceptions[0] - checkDataContract(t, "ExceptionDetails.Message", exd3.Message, "My go stringer error") - checkDataContract(t, "ExceptionDetails.TypeName", exd3.TypeName, "*appinsights.myGoStringer") -} - -func TestTrackPanic(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - - catchTrackPanic(client, "~exception~") - client.Channel().Close() - - req := transmitter.waitForRequest(t) - if !strings.Contains(req.payload, "~exception~") { - t.Error("Unexpected payload") - } -} - -func testExceptionCallstack(t *testing.T, n int) *contracts.ExceptionDetails { - d := buildStack(n).TelemetryData().(*contracts.ExceptionData) - checkDataContract(t, "len(Exceptions)", len(d.Exceptions), 1) - ex := d.Exceptions[0] - - // Find the relevant range of frames - frstart := -1 - frend := -1 - for i, f := range ex.ParsedStack { - if strings.Contains(f.Method, "Collatz") { - if frstart < 0 { - frstart = i - } - } else { - if frstart >= 0 && frend < 0 { - frend = i - break - } - } - } - - expected := collatzFrames(n) - - if frend-frstart != len(expected) { - t.Errorf("Wrong number of Collatz frames found. Got %d, want %d.", frend-frstart, len(expected)) - return ex - } - - j := len(expected) - 1 - for i := frstart; j >= 0 && i < len(ex.ParsedStack); i++ { - checkDataContract(t, fmt.Sprintf("ParsedStack[%d].Method", i), ex.ParsedStack[i].Method, expected[j]) - if !strings.HasSuffix(ex.ParsedStack[i].Assembly, "/ApplicationInsights-Go/appinsights") { - checkDataContract(t, fmt.Sprintf("ParsedStack[%d].Assembly", i), ex.ParsedStack[i].Assembly, "/ApplicationInsights-Go/appinsights") - } - if !strings.HasSuffix(ex.ParsedStack[i].FileName, "/exception_test.go") { - checkDataContract(t, fmt.Sprintf("ParsedStack[%d].FileName", i), ex.ParsedStack[i].FileName, "exception_test.go") - } - - j-- - } - - return ex -} - -func collatzFrames(n int) []string { - var result []string - - result = append(result, "panicTestCollatz") - for n != 1 { - if (n % 2) == 0 { - result = append(result, "panicTestCollatzEven") - n /= 2 - } else { - result = append(result, "panicTestCollatzOdd") - n = 1 + (3 * n) - } - - result = append(result, "panicTestCollatz") - } - - return result -} - -func catchPanic(err interface{}) *ExceptionTelemetry { - var result *ExceptionTelemetry - - func() { - defer func() { - if err := recover(); err != nil { - result = NewExceptionTelemetry(err) - } - }() - - panic(err) - }() - - return result -} - -func buildStack(n int) *ExceptionTelemetry { - var result *ExceptionTelemetry - - // Nest this so the panic doesn't supercede the return - func() { - defer func() { - if err := recover(); err != nil { - result = NewExceptionTelemetry(err) - } - }() - - panicTestCollatz(n) - }() - - return result -} - -// Test Collatz's conjecture for a given input; panic in the base case. -func panicTestCollatz(n int) int { - if n == 1 { - panic("Whoops") - } - - if (n & 1) == 0 { - return panicTestCollatzEven(n) - } else { - return panicTestCollatzOdd(n) - } -} - -func panicTestCollatzEven(n int) int { - return panicTestCollatz(n / 2) -} - -func panicTestCollatzOdd(n int) int { - return panicTestCollatz((3 * n) + 1) -} - -func catchTrackPanic(client TelemetryClient, err interface{}) { - defer TrackPanic(client, false) - panic(err) -} diff --git a/application-insights/appinsights/inmemorychannel.go b/application-insights/appinsights/inmemorychannel.go deleted file mode 100644 index 4296e4c899..0000000000 --- a/application-insights/appinsights/inmemorychannel.go +++ /dev/null @@ -1,449 +0,0 @@ -package appinsights - -import ( - "sync" - "time" - - "code.cloudfoundry.org/clock" - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -var ( - submit_retries = []time.Duration{time.Duration(10 * time.Second), time.Duration(30 * time.Second), time.Duration(60 * time.Second)} -) - -// A telemetry channel that stores events exclusively in memory. Presently -// the only telemetry channel implementation available. -type InMemoryChannel struct { - endpointAddress string - isDeveloperMode bool - collectChan chan *contracts.Envelope - controlChan chan *inMemoryChannelControl - batchSize int - batchInterval time.Duration - waitgroup sync.WaitGroup - throttle *throttleManager - transmitter transmitter -} - -type inMemoryChannelControl struct { - // If true, flush the buffer. - flush bool - - // If true, stop listening on the channel. (Flush is required if any events are to be sent) - stop bool - - // If stopping and flushing, this specifies whether to retry submissions on error. - retry bool - - // If retrying, what is the max time to wait before finishing up? - timeout time.Duration - - // If specified, a message will be sent on this channel when all pending telemetry items have been submitted - callback chan struct{} -} - -// Creates an InMemoryChannel instance and starts a background submission -// goroutine. -func NewInMemoryChannel(config *TelemetryConfiguration) *InMemoryChannel { - channel := &InMemoryChannel{ - endpointAddress: config.EndpointUrl, - collectChan: make(chan *contracts.Envelope), - controlChan: make(chan *inMemoryChannelControl), - batchSize: config.MaxBatchSize, - batchInterval: config.MaxBatchInterval, - throttle: newThrottleManager(), - transmitter: newTransmitter(config.EndpointUrl, config.Client), - } - - go channel.acceptLoop() - - return channel -} - -// The address of the endpoint to which telemetry is sent -func (channel *InMemoryChannel) EndpointAddress() string { - return channel.endpointAddress -} - -// Queues a single telemetry item -func (channel *InMemoryChannel) Send(item *contracts.Envelope) { - if item != nil && channel.collectChan != nil { - channel.collectChan <- item - } -} - -// Forces the current queue to be sent -func (channel *InMemoryChannel) Flush() { - if channel.controlChan != nil { - channel.controlChan <- &inMemoryChannelControl{ - flush: true, - } - } -} - -// Tears down the submission goroutines, closes internal channels. Any -// telemetry waiting to be sent is discarded. Further calls to Send() have -// undefined behavior. This is a more abrupt version of Close(). -func (channel *InMemoryChannel) Stop() { - if channel.controlChan != nil { - channel.controlChan <- &inMemoryChannelControl{ - stop: true, - } - } -} - -// Returns true if this channel has been throttled by the data collector. -func (channel *InMemoryChannel) IsThrottled() bool { - return channel.throttle != nil && channel.throttle.IsThrottled() -} - -// Flushes and tears down the submission goroutine and closes internal -// channels. Returns a channel that is closed when all pending telemetry -// items have been submitted and it is safe to shut down without losing -// telemetry. -// -// If retryTimeout is specified and non-zero, then failed submissions will -// be retried until one succeeds or the timeout expires, whichever occurs -// first. A retryTimeout of zero indicates that failed submissions will be -// retried as usual. An omitted retryTimeout indicates that submissions -// should not be retried if they fail. -// -// Note that the returned channel may not be closed before retryTimeout even -// if it is specified. This is because retryTimeout only applies to the -// latest telemetry buffer. This may be typical for applications that -// submit a large amount of telemetry or are prone to being throttled. When -// exiting, you should select on the result channel and your own timer to -// avoid long delays. -func (channel *InMemoryChannel) Close(timeout ...time.Duration) <-chan struct{} { - if channel.controlChan != nil { - callback := make(chan struct{}) - - ctl := &inMemoryChannelControl{ - stop: true, - flush: true, - retry: false, - callback: callback, - } - - if len(timeout) > 0 { - ctl.retry = true - ctl.timeout = timeout[0] - } - - channel.controlChan <- ctl - - return callback - } else { - return nil - } -} - -func (channel *InMemoryChannel) acceptLoop() { - channelState := newInMemoryChannelState(channel) - - for !channelState.stopping { - channelState.start() - } - - channelState.stop() -} - -// Data shared between parts of a channel -type inMemoryChannelState struct { - channel *InMemoryChannel - stopping bool - buffer telemetryBufferItems - retry bool - retryTimeout time.Duration - callback chan struct{} - timer clock.Timer -} - -func newInMemoryChannelState(channel *InMemoryChannel) *inMemoryChannelState { - // Initialize timer to stopped -- avoid any chance of a race condition. - timer := currentClock.NewTimer(time.Hour) - timer.Stop() - - return &inMemoryChannelState{ - channel: channel, - buffer: make(telemetryBufferItems, 0, 16), - stopping: false, - timer: timer, - } -} - -// Part of channel accept loop: Initialize buffer and accept first message, handle controls. -func (state *inMemoryChannelState) start() bool { - if len(state.buffer) > 16 { - // Start out with the size of the previous buffer - state.buffer = make(telemetryBufferItems, 0, cap(state.buffer)) - } else if len(state.buffer) > 0 { - // Start out with at least 16 slots - state.buffer = make(telemetryBufferItems, 0, 16) - } - - // Wait for an event - select { - case event := <-state.channel.collectChan: - if event == nil { - // Channel closed? Not intercepted by Send()? - panic("Received nil event") - } - - state.buffer = append(state.buffer, event) - - case ctl := <-state.channel.controlChan: - // The buffer is empty, so there would be no point in flushing - state.channel.signalWhenDone(ctl.callback) - - if ctl.stop { - state.stopping = true - return false - } - } - - if len(state.buffer) == 0 { - return true - } - - return state.waitToSend() -} - -// Part of channel accept loop: Wait for buffer to fill, timeout to expire, or flush -func (state *inMemoryChannelState) waitToSend() bool { - // Things that are used by the sender if we receive a control message - state.retryTimeout = 0 - state.retry = true - state.callback = nil - - // Delay until timeout passes or buffer fills up - state.timer.Reset(state.channel.batchInterval) - - for { - if len(state.buffer) >= state.channel.batchSize { - if !state.timer.Stop() { - <-state.timer.C() - } - - return state.send() - } - - select { - case event := <-state.channel.collectChan: - if event == nil { - // Channel closed? Not intercepted by Send()? - panic("Received nil event") - } - - state.buffer = append(state.buffer, event) - - case ctl := <-state.channel.controlChan: - if ctl.stop { - state.stopping = true - state.retry = ctl.retry - if !ctl.flush { - // No flush? Just exit. - state.channel.signalWhenDone(ctl.callback) - return false - } - } - - if ctl.flush { - if !state.timer.Stop() { - <-state.timer.C() - } - - state.retryTimeout = ctl.timeout - state.callback = ctl.callback - return state.send() - } - - case <-state.timer.C(): - // Timeout expired - return state.send() - } - } -} - -// Part of channel accept loop: Check and wait on throttle, submit pending telemetry -func (state *inMemoryChannelState) send() bool { - // Hold up transmission if we're being throttled - if !state.stopping && state.channel.throttle.IsThrottled() { - if !state.waitThrottle() { - // Stopped - return false - } - } - - // Send - if len(state.buffer) > 0 { - state.channel.waitgroup.Add(1) - - // If we have a callback, wait on the waitgroup now that it's - // incremented. - state.channel.signalWhenDone(state.callback) - - go func(buffer telemetryBufferItems, retry bool, retryTimeout time.Duration) { - defer state.channel.waitgroup.Done() - state.channel.transmitRetry(buffer, retry, retryTimeout) - }(state.buffer, state.retry, state.retryTimeout) - } else if state.callback != nil { - state.channel.signalWhenDone(state.callback) - } - - return true -} - -// Part of channel accept loop: Wait for throttle to expire while dropping messages -func (state *inMemoryChannelState) waitThrottle() bool { - // Channel is currently throttled. Once the buffer fills, messages will - // be lost... If we're exiting, then we'll just try to submit anyway. That - // request may be throttled and transmitRetry will perform the backoff correctly. - - diagnosticsWriter.Write("Channel is throttled, events may be dropped.") - throttleDone := state.channel.throttle.NotifyWhenReady() - dropped := 0 - - defer diagnosticsWriter.Printf("Channel dropped %d events while throttled", dropped) - - for { - select { - case <-throttleDone: - close(throttleDone) - return true - - case event := <-state.channel.collectChan: - // If there's still room in the buffer, then go ahead and add it. - if len(state.buffer) < state.channel.batchSize { - state.buffer = append(state.buffer, event) - } else { - if dropped == 0 { - diagnosticsWriter.Write("Buffer is full, dropping further events.") - } - - dropped++ - } - - case ctl := <-state.channel.controlChan: - if ctl.stop { - state.stopping = true - state.retry = ctl.retry - if !ctl.flush { - state.channel.signalWhenDone(ctl.callback) - return false - } else { - // Make an exception when stopping - return true - } - } - - // Cannot flush - // TODO: Figure out what to do about callback? - if ctl.flush { - state.channel.signalWhenDone(ctl.callback) - } - } - } -} - -// Part of channel accept loop: Clean up and close telemetry channel -func (state *inMemoryChannelState) stop() { - close(state.channel.collectChan) - close(state.channel.controlChan) - - state.channel.collectChan = nil - state.channel.controlChan = nil - - // Throttle can't close until transmitters are done using it. - state.channel.waitgroup.Wait() - state.channel.throttle.Stop() - - state.channel.throttle = nil -} - -func (channel *InMemoryChannel) transmitRetry(items telemetryBufferItems, retry bool, retryTimeout time.Duration) { - payload := items.serialize() - retryTimeRemaining := retryTimeout - - for _, wait := range submit_retries { - result, err := channel.transmitter.Transmit(payload, items) - if err == nil && result != nil && result.IsSuccess() { - return - } - - if !retry { - diagnosticsWriter.Write("Refusing to retry telemetry submission (retry==false)") - return - } - - // Check for success, determine if we need to retry anything - if result != nil { - if result.CanRetry() { - // Filter down to failed items - payload, items = result.GetRetryItems(payload, items) - if len(payload) == 0 || len(items) == 0 { - return - } - } else { - diagnosticsWriter.Write("Cannot retry telemetry submission") - return - } - - // Check for throttling - if result.IsThrottled() { - if result.retryAfter != nil { - diagnosticsWriter.Printf("Channel is throttled until %s", *result.retryAfter) - channel.throttle.RetryAfter(*result.retryAfter) - } else { - // TODO: Pick a time - } - } - } - - if retryTimeout > 0 { - // We're on a time schedule here. Make sure we don't try longer - // than we have been allowed. - if retryTimeRemaining < wait { - // One more chance left -- we'll wait the max time we can - // and then retry on the way out. - currentClock.Sleep(retryTimeRemaining) - break - } else { - // Still have time left to go through the rest of the regular - // retry schedule - retryTimeRemaining -= wait - } - } - - diagnosticsWriter.Printf("Waiting %s to retry submission", wait) - currentClock.Sleep(wait) - - // Wait if the channel is throttled and we're not on a schedule - if channel.IsThrottled() && retryTimeout == 0 { - diagnosticsWriter.Printf("Channel is throttled; extending wait time.") - ch := channel.throttle.NotifyWhenReady() - result := <-ch - close(ch) - - if !result { - return - } - } - } - - // One final try - _, err := channel.transmitter.Transmit(payload, items) - if err != nil { - diagnosticsWriter.Write("Gave up transmitting payload; exhausted retries") - } -} - -func (channel *InMemoryChannel) signalWhenDone(callback chan struct{}) { - if callback != nil { - go func() { - channel.waitgroup.Wait() - close(callback) - }() - } -} diff --git a/application-insights/appinsights/inmemorychannel_test.go b/application-insights/appinsights/inmemorychannel_test.go deleted file mode 100644 index e02e11c699..0000000000 --- a/application-insights/appinsights/inmemorychannel_test.go +++ /dev/null @@ -1,628 +0,0 @@ -package appinsights - -import ( - "fmt" - "strings" - "testing" - "time" -) - -const ten_seconds = time.Duration(10) * time.Second - -type testTransmitter struct { - requests chan *testTransmission - responses chan *transmissionResult -} - -func (transmitter *testTransmitter) Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) { - itemsCopy := make(telemetryBufferItems, len(items)) - copy(itemsCopy, items) - - transmitter.requests <- &testTransmission{ - payload: string(payload), - items: itemsCopy, - timestamp: currentClock.Now(), - } - - return <-transmitter.responses, nil -} - -func (transmitter *testTransmitter) Close() { - close(transmitter.requests) - close(transmitter.responses) -} - -func (transmitter *testTransmitter) prepResponse(statusCodes ...int) { - for _, code := range statusCodes { - transmitter.responses <- &transmissionResult{statusCode: code} - } -} - -func (transmitter *testTransmitter) prepThrottle(after time.Duration) time.Time { - retryAfter := currentClock.Now().Add(after) - - transmitter.responses <- &transmissionResult{ - statusCode: 408, - retryAfter: &retryAfter, - } - - return retryAfter -} - -func (transmitter *testTransmitter) waitForRequest(t *testing.T) *testTransmission { - select { - case req := <-transmitter.requests: - return req - case <-time.After(time.Duration(500) * time.Millisecond): - t.Fatal("Timed out waiting for request to be sent") - return nil /* Not reached */ - } -} - -func (transmitter *testTransmitter) assertNoRequest(t *testing.T) { - select { - case <-transmitter.requests: - t.Fatal("Expected no request") - case <-time.After(time.Duration(10) * time.Millisecond): - return - } -} - -type testTransmission struct { - timestamp time.Time - payload string - items telemetryBufferItems -} - -func newTestChannelServer(config ...*TelemetryConfiguration) (TelemetryClient, *testTransmitter) { - transmitter := &testTransmitter{ - requests: make(chan *testTransmission, 16), - responses: make(chan *transmissionResult, 16), - } - - var client TelemetryClient - if len(config) > 0 { - client = NewTelemetryClientFromConfig(config[0]) - } else { - config := NewTelemetryConfiguration("") - config.MaxBatchInterval = ten_seconds // assumed by every test. - client = NewTelemetryClientFromConfig(config) - } - - client.(*telemetryClient).channel.(*InMemoryChannel).transmitter = transmitter - - return client, transmitter -} - -func assertTimeApprox(t *testing.T, x, y time.Time) { - const delta = (time.Duration(100) * time.Millisecond) - if (x.Before(y) && y.Sub(x) > delta) || (y.Before(x) && x.Sub(y) > delta) { - t.Errorf("Time isn't a close match: %v vs %v", x, y) - } -} - -func assertNotClosed(t *testing.T, ch <-chan struct{}) { - select { - case <-ch: - t.Fatal("Close signal was not expected to be received") - default: - } -} - -func waitForClose(t *testing.T, ch <-chan struct{}) bool { - select { - case <-ch: - return true - case <-time.After(time.Duration(100) * time.Second): - t.Fatal("Close signal not received in 100ms") - return false /* not reached */ - } -} - -func TestSimpleSubmit(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - defer client.Channel().Stop() - - client.TrackTrace("~msg~", Information) - tm := currentClock.Now() - transmitter.prepResponse(200) - - slowTick(11) - req := transmitter.waitForRequest(t) - - assertTimeApprox(t, req.timestamp, tm.Add(ten_seconds)) - - if !strings.Contains(string(req.payload), "~msg~") { - t.Errorf("Payload does not contain message") - } -} - -func TestMultipleSubmit(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - defer client.Channel().Stop() - - transmitter.prepResponse(200, 200) - - start := currentClock.Now() - - for i := 0; i < 16; i++ { - client.TrackTrace(fmt.Sprintf("~msg-%x~", i), Information) - slowTick(1) - } - - slowTick(10) - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, start.Add(ten_seconds)) - - for i := 0; i < 10; i++ { - if !strings.Contains(req1.payload, fmt.Sprintf("~msg-%x~", i)) { - t.Errorf("Payload does not contain expected item: %x", i) - } - } - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, start.Add(ten_seconds+ten_seconds)) - - for i := 10; i < 16; i++ { - if !strings.Contains(req2.payload, fmt.Sprintf("~msg-%x~", i)) { - t.Errorf("Payload does not contain expected item: %x", i) - } - } -} - -func TestFlush(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - defer client.Channel().Stop() - - transmitter.prepResponse(200, 200) - - // Empty flush should do nothing - client.Channel().Flush() - - tm := currentClock.Now() - client.TrackTrace("~msg~", Information) - client.Channel().Flush() - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, tm) - if !strings.Contains(req1.payload, "~msg~") { - t.Error("Unexpected payload") - } - - // Next one goes back to normal - client.TrackTrace("~next~", Information) - slowTick(11) - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds)) - if !strings.Contains(req2.payload, "~next~") { - t.Error("Unexpected payload") - } -} - -func TestStop(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - - transmitter.prepResponse(200) - - client.TrackTrace("Not sent", Information) - client.Channel().Stop() - slowTick(20) - transmitter.assertNoRequest(t) -} - -func TestCloseFlush(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - - transmitter.prepResponse(200) - - client.TrackTrace("~flushed~", Information) - client.Channel().Close() - - req := transmitter.waitForRequest(t) - if !strings.Contains(req.payload, "~flushed~") { - t.Error("Unexpected payload") - } -} - -func TestCloseFlushRetry(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - - transmitter.prepResponse(500, 200) - - client.TrackTrace("~flushed~", Information) - tm := currentClock.Now() - ch := client.Channel().Close(time.Minute) - - slowTick(30) - - waitForClose(t, ch) - - req1 := transmitter.waitForRequest(t) - if !strings.Contains(req1.payload, "~flushed~") { - t.Error("Unexpected payload") - } - - assertTimeApprox(t, req1.timestamp, tm) - - req2 := transmitter.waitForRequest(t) - if !strings.Contains(req2.payload, "~flushed~") { - t.Error("Unexpected payload") - } - - assertTimeApprox(t, req2.timestamp, tm.Add(submit_retries[0])) -} - -func TestCloseWithOngoingRetry(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer transmitter.Close() - - transmitter.prepResponse(408, 200, 200) - - // This message should get stuck, retried - client.TrackTrace("~msg-1~", Information) - slowTick(11) - - // Check first one came through - req1 := transmitter.waitForRequest(t) - if !strings.Contains(req1.payload, "~msg-1~") { - t.Error("First message unexpected payload") - } - - // This message will get flushed immediately - client.TrackTrace("~msg-2~", Information) - ch := client.Channel().Close(time.Minute) - - // Let 2 go out, but not the retry for 1 - slowTick(3) - - assertNotClosed(t, ch) - - req2 := transmitter.waitForRequest(t) - if !strings.Contains(req2.payload, "~msg-2~") { - t.Error("Second message unexpected payload") - } - - // Then, let's wait for the first message to go out... - slowTick(20) - - waitForClose(t, ch) - - req3 := transmitter.waitForRequest(t) - if !strings.Contains(req3.payload, "~msg-1~") { - t.Error("Third message unexpected payload") - } -} - -func TestSendOnBufferFull(t *testing.T) { - mockClock() - defer resetClock() - - config := NewTelemetryConfiguration("") - config.MaxBatchSize = 4 - client, transmitter := newTestChannelServer(config) - defer transmitter.Close() - defer client.Channel().Stop() - - transmitter.prepResponse(200, 200) - - for i := 0; i < 5; i++ { - client.TrackTrace(fmt.Sprintf("~msg-%d~", i), Information) - } - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, currentClock.Now()) - - for i := 0; i < 4; i++ { - if !strings.Contains(req1.payload, fmt.Sprintf("~msg-%d~", i)) || len(req1.items) != 4 { - t.Errorf("Payload does not contain expected message") - } - } - - slowTick(5) - transmitter.assertNoRequest(t) - slowTick(5) - - // The last one should have gone out as normal - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, currentClock.Now()) - if !strings.Contains(req2.payload, "~msg-4~") || len(req2.items) != 1 { - t.Errorf("Payload does not contain expected message") - } -} - -func TestRetryOnFailure(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer client.Channel().Stop() - defer transmitter.Close() - - transmitter.prepResponse(500, 200) - - client.TrackTrace("~msg-1~", Information) - client.TrackTrace("~msg-2~", Information) - - tm := currentClock.Now() - slowTick(10) - - req1 := transmitter.waitForRequest(t) - if !strings.Contains(req1.payload, "~msg-1~") || !strings.Contains(req1.payload, "~msg-2~") || len(req1.items) != 2 { - t.Error("Unexpected payload") - } - - assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) - - slowTick(30) - - req2 := transmitter.waitForRequest(t) - if req2.payload != req1.payload || len(req2.items) != 2 { - t.Error("Unexpected payload") - } - - assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds).Add(submit_retries[0])) -} - -func TestPartialRetry(t *testing.T) { - mockClock() - defer resetClock() - client, transmitter := newTestChannelServer() - defer client.Channel().Stop() - defer transmitter.Close() - - client.TrackTrace("~ok-1~", Information) - client.TrackTrace("~retry-1~", Information) - client.TrackTrace("~ok-2~", Information) - client.TrackTrace("~bad-1~", Information) - client.TrackTrace("~retry-2~", Information) - - transmitter.responses <- &transmissionResult{ - statusCode: 206, - response: &backendResponse{ - ItemsAccepted: 2, - ItemsReceived: 5, - Errors: []*itemTransmissionResult{ - &itemTransmissionResult{Index: 1, StatusCode: 500, Message: "Server Error"}, - &itemTransmissionResult{Index: 2, StatusCode: 200, Message: "OK"}, - &itemTransmissionResult{Index: 3, StatusCode: 400, Message: "Bad Request"}, - &itemTransmissionResult{Index: 4, StatusCode: 408, Message: "Plz Retry"}, - }, - }, - } - - transmitter.prepResponse(200) - - tm := currentClock.Now() - slowTick(30) - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) - if len(req1.items) != 5 { - t.Error("Unexpected payload") - } - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds).Add(submit_retries[0])) - if len(req2.items) != 2 { - t.Error("Unexpected payload") - } - - if strings.Contains(req2.payload, "~ok-") || strings.Contains(req2.payload, "~bad-") || !strings.Contains(req2.payload, "~retry-") { - t.Error("Unexpected payload") - } -} - -func TestThrottleDropsMessages(t *testing.T) { - mockClock() - defer resetClock() - config := NewTelemetryConfiguration("") - config.MaxBatchSize = 4 - client, transmitter := newTestChannelServer(config) - defer client.Channel().Stop() - defer transmitter.Close() - - tm := currentClock.Now() - retryAfter := transmitter.prepThrottle(time.Minute) - transmitter.prepResponse(200, 200) - - client.TrackTrace("~throttled~", Information) - slowTick(10) - - for i := 0; i < 20; i++ { - client.TrackTrace(fmt.Sprintf("~msg-%d~", i), Information) - } - - slowTick(60) - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) - if len(req1.items) != 1 || !strings.Contains(req1.payload, "~throttled~") || strings.Contains(req1.payload, "~msg-") { - t.Error("Unexpected payload") - } - - // Humm.. this might break- these two could flip places. But I haven't seen it happen yet. - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, retryAfter) - if len(req2.items) != 1 || !strings.Contains(req2.payload, "~throttled~") || strings.Contains(req2.payload, "~msg-") { - t.Error("Unexpected payload") - } - - req3 := transmitter.waitForRequest(t) - assertTimeApprox(t, req3.timestamp, retryAfter) - if len(req3.items) != 4 || strings.Contains(req3.payload, "~throttled-") || !strings.Contains(req3.payload, "~msg-") { - t.Error("Unexpected payload") - } - - transmitter.assertNoRequest(t) -} - -func TestThrottleCannotFlush(t *testing.T) { - mockClock() - defer resetClock() - config := NewTelemetryConfiguration("") - config.MaxBatchSize = 4 - client, transmitter := newTestChannelServer(config) - defer client.Channel().Stop() - defer transmitter.Close() - - tm := currentClock.Now() - retryAfter := transmitter.prepThrottle(time.Minute) - - transmitter.prepResponse(200, 200) - - client.TrackTrace("~throttled~", Information) - slowTick(10) - - client.TrackTrace("~msg~", Information) - client.Channel().Flush() - - slowTick(60) - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, retryAfter) - - req3 := transmitter.waitForRequest(t) - assertTimeApprox(t, req3.timestamp, retryAfter) - - transmitter.assertNoRequest(t) -} - -func TestThrottleFlushesOnClose(t *testing.T) { - mockClock() - defer resetClock() - config := NewTelemetryConfiguration("") - config.MaxBatchSize = 4 - client, transmitter := newTestChannelServer(config) - defer transmitter.Close() - - tm := currentClock.Now() - retryAfter := transmitter.prepThrottle(time.Minute) - - transmitter.prepResponse(200, 200) - - client.TrackTrace("~throttled~", Information) - slowTick(10) - - client.TrackTrace("~msg~", Information) - ch := client.Channel().Close(30 * time.Second) - - slowTick(60) - - waitForClose(t, ch) - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, tm.Add(ten_seconds)) - if !strings.Contains(req1.payload, "~throttled~") || len(req1.items) != 1 { - t.Error("Unexpected payload") - } - - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, tm.Add(ten_seconds)) - if !strings.Contains(req2.payload, "~msg~") || len(req2.items) != 1 { - t.Error("Unexpected payload") - } - - req3 := transmitter.waitForRequest(t) - assertTimeApprox(t, req3.timestamp, retryAfter) - if !strings.Contains(req3.payload, "~throttled~") || len(req3.items) != 1 { - t.Error("Unexpected payload") - } - - transmitter.assertNoRequest(t) -} - -func TestThrottleAbandonsMessageOnStop(t *testing.T) { - mockClock() - defer resetClock() - config := NewTelemetryConfiguration("") - config.MaxBatchSize = 4 - client, transmitter := newTestChannelServer(config) - defer transmitter.Close() - - transmitter.prepThrottle(time.Minute) - transmitter.prepResponse(200, 200, 200, 200) - - client.TrackTrace("~throttled~", Information) - slowTick(10) - client.TrackTrace("~dropped~", Information) - slowTick(10) - client.Channel().Stop() - slowTick(45) - - // ~throttled~ will get retried after throttle is done; ~dropped~ should get lost. - for i := 0; i < 2; i++ { - req := transmitter.waitForRequest(t) - if strings.Contains(req.payload, "~dropped~") || len(req.items) != 1 { - t.Fatal("Dropped should have never been sent") - } - } - - transmitter.assertNoRequest(t) -} - -func TestThrottleStacking(t *testing.T) { - mockClock() - defer resetClock() - config := NewTelemetryConfiguration("") - config.MaxBatchSize = 1 - client, transmitter := newTestChannelServer(config) - defer transmitter.Close() - - // It's easy to hit a race in this test. There are two places that check for - // a throttle: one in the channel accept loop, the other in transmitRetry. - // For this test, I want both to hit the one in transmitRetry and then each - // make further attempts in lock-step from there. - - start := currentClock.Now() - client.TrackTrace("~throttle-1~", Information) - client.TrackTrace("~throttle-2~", Information) - - // Per above, give both time to get to transmitRetry, then send out responses - // simultaneously. - slowTick(10) - - transmitter.prepThrottle(20 * time.Second) - second_tm := transmitter.prepThrottle(time.Minute) - - transmitter.prepResponse(200, 200, 200) - - slowTick(65) - - req1 := transmitter.waitForRequest(t) - assertTimeApprox(t, req1.timestamp, start) - req2 := transmitter.waitForRequest(t) - assertTimeApprox(t, req2.timestamp, start) - - req3 := transmitter.waitForRequest(t) - assertTimeApprox(t, req3.timestamp, second_tm) - req4 := transmitter.waitForRequest(t) - assertTimeApprox(t, req4.timestamp, second_tm) - - transmitter.assertNoRequest(t) -} diff --git a/application-insights/appinsights/jsonserializer.go b/application-insights/appinsights/jsonserializer.go deleted file mode 100644 index 4706cd764d..0000000000 --- a/application-insights/appinsights/jsonserializer.go +++ /dev/null @@ -1,25 +0,0 @@ -package appinsights - -import ( - "bytes" - "encoding/json" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -type telemetryBufferItems []*contracts.Envelope - -func (items telemetryBufferItems) serialize() []byte { - var result bytes.Buffer - encoder := json.NewEncoder(&result) - - for _, item := range items { - end := result.Len() - if err := encoder.Encode(item); err != nil { - diagnosticsWriter.Printf("Telemetry item failed to serialize: %s", err.Error()) - result.Truncate(end) - } - } - - return result.Bytes() -} diff --git a/application-insights/appinsights/jsonserializer_test.go b/application-insights/appinsights/jsonserializer_test.go deleted file mode 100644 index 015b61db49..0000000000 --- a/application-insights/appinsights/jsonserializer_test.go +++ /dev/null @@ -1,483 +0,0 @@ -package appinsights - -import ( - "bytes" - "encoding/json" - "fmt" - "math" - "strconv" - "strings" - "testing" - "time" -) - -const test_ikey = "01234567-0000-89ab-cdef-000000000000" - -func TestJsonSerializerEvents(t *testing.T) { - mockClock(time.Unix(1511001321, 0)) - defer resetClock() - - var buffer telemetryBufferItems - - buffer.add( - NewTraceTelemetry("testing", Error), - NewEventTelemetry("an-event"), - NewMetricTelemetry("a-metric", 567), - ) - - req := NewRequestTelemetry("method", "my-url", time.Minute, "204") - req.Name = "req-name" - req.Id = "my-id" - buffer.add(req) - - agg := NewAggregateMetricTelemetry("agg-metric") - agg.AddData([]float64{1, 2, 3}) - buffer.add(agg) - - remdep := NewRemoteDependencyTelemetry("bing-remote-dep", "http", "www.bing.com", false) - remdep.Data = "some-data" - remdep.ResultCode = "arg" - remdep.Duration = 4 * time.Second - remdep.Properties["hi"] = "hello" - buffer.add(remdep) - - avail := NewAvailabilityTelemetry("webtest", 8*time.Second, true) - avail.RunLocation = "jupiter" - avail.Message = "ok." - avail.Measurements["measure"] = 88.0 - avail.Id = "avail-id" - buffer.add(avail) - - view := NewPageViewTelemetry("name", "http://bing.com") - view.Duration = 4 * time.Minute - buffer.add(view) - - j, err := parsePayload(buffer.serialize()) - if err != nil { - t.Errorf("Error parsing payload: %s", err.Error()) - } - - if len(j) != 8 { - t.Fatal("Unexpected event count") - } - - // Trace - j[0].assertPath(t, "iKey", test_ikey) - j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") - j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[0].assertPath(t, "sampleRate", 100.0) - j[0].assertPath(t, "data.baseType", "MessageData") - j[0].assertPath(t, "data.baseData.message", "testing") - j[0].assertPath(t, "data.baseData.severityLevel", 3) - j[0].assertPath(t, "data.baseData.ver", 2) - - // Event - j[1].assertPath(t, "iKey", test_ikey) - j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") - j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[1].assertPath(t, "sampleRate", 100.0) - j[1].assertPath(t, "data.baseType", "EventData") - j[1].assertPath(t, "data.baseData.name", "an-event") - j[1].assertPath(t, "data.baseData.ver", 2) - - // Metric - j[2].assertPath(t, "iKey", test_ikey) - j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") - j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[2].assertPath(t, "sampleRate", 100.0) - j[2].assertPath(t, "data.baseType", "MetricData") - j[2].assertPath(t, "data.baseData.metrics.", 1) - j[2].assertPath(t, "data.baseData.metrics.[0].value", 567) - j[2].assertPath(t, "data.baseData.metrics.[0].count", 1) - j[2].assertPath(t, "data.baseData.metrics.[0].kind", 0) - j[2].assertPath(t, "data.baseData.metrics.[0].name", "a-metric") - j[2].assertPath(t, "data.baseData.ver", 2) - - // Request - j[3].assertPath(t, "iKey", test_ikey) - j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") - j[3].assertPath(t, "time", "2017-11-18T10:34:21Z") // Constructor subtracts duration - j[3].assertPath(t, "sampleRate", 100.0) - j[3].assertPath(t, "data.baseType", "RequestData") - j[3].assertPath(t, "data.baseData.name", "req-name") - j[3].assertPath(t, "data.baseData.duration", "0.00:01:00.0000000") - j[3].assertPath(t, "data.baseData.responseCode", "204") - j[3].assertPath(t, "data.baseData.success", true) - j[3].assertPath(t, "data.baseData.id", "my-id") - j[3].assertPath(t, "data.baseData.url", "my-url") - j[3].assertPath(t, "data.baseData.ver", 2) - - // Aggregate metric - j[4].assertPath(t, "iKey", test_ikey) - j[4].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") - j[4].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[4].assertPath(t, "sampleRate", 100.0) - j[4].assertPath(t, "data.baseType", "MetricData") - j[4].assertPath(t, "data.baseData.metrics.", 1) - j[4].assertPath(t, "data.baseData.metrics.[0].value", 6) - j[4].assertPath(t, "data.baseData.metrics.[0].count", 3) - j[4].assertPath(t, "data.baseData.metrics.[0].kind", 1) - j[4].assertPath(t, "data.baseData.metrics.[0].min", 1) - j[4].assertPath(t, "data.baseData.metrics.[0].max", 3) - j[4].assertPath(t, "data.baseData.metrics.[0].stdDev", 0.8164) - j[4].assertPath(t, "data.baseData.metrics.[0].name", "agg-metric") - j[4].assertPath(t, "data.baseData.ver", 2) - - // Remote dependency - j[5].assertPath(t, "iKey", test_ikey) - j[5].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.RemoteDependency") - j[5].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[5].assertPath(t, "sampleRate", 100.0) - j[5].assertPath(t, "data.baseType", "RemoteDependencyData") - j[5].assertPath(t, "data.baseData.name", "bing-remote-dep") - j[5].assertPath(t, "data.baseData.id", "") - j[5].assertPath(t, "data.baseData.resultCode", "arg") - j[5].assertPath(t, "data.baseData.duration", "0.00:00:04.0000000") - j[5].assertPath(t, "data.baseData.success", false) - j[5].assertPath(t, "data.baseData.data", "some-data") - j[5].assertPath(t, "data.baseData.target", "www.bing.com") - j[5].assertPath(t, "data.baseData.type", "http") - j[5].assertPath(t, "data.baseData.properties.hi", "hello") - j[5].assertPath(t, "data.baseData.ver", 2) - - // Availability - j[6].assertPath(t, "iKey", test_ikey) - j[6].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Availability") - j[6].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[6].assertPath(t, "sampleRate", 100.0) - j[6].assertPath(t, "data.baseType", "AvailabilityData") - j[6].assertPath(t, "data.baseData.name", "webtest") - j[6].assertPath(t, "data.baseData.duration", "0.00:00:08.0000000") - j[6].assertPath(t, "data.baseData.success", true) - j[6].assertPath(t, "data.baseData.runLocation", "jupiter") - j[6].assertPath(t, "data.baseData.message", "ok.") - j[6].assertPath(t, "data.baseData.id", "avail-id") - j[6].assertPath(t, "data.baseData.ver", 2) - j[6].assertPath(t, "data.baseData.measurements.measure", 88) - - // Page view - j[7].assertPath(t, "iKey", test_ikey) - j[7].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.PageView") - j[7].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[7].assertPath(t, "sampleRate", 100.0) - j[7].assertPath(t, "data.baseType", "PageViewData") - j[7].assertPath(t, "data.baseData.name", "name") - j[7].assertPath(t, "data.baseData.url", "http://bing.com") - j[7].assertPath(t, "data.baseData.duration", "0.00:04:00.0000000") - j[7].assertPath(t, "data.baseData.ver", 2) -} - -func TestJsonSerializerNakedEvents(t *testing.T) { - mockClock(time.Unix(1511001321, 0)) - defer resetClock() - - var buffer telemetryBufferItems - - buffer.add( - &TraceTelemetry{ - Message: "Naked telemetry", - SeverityLevel: Warning, - }, - &EventTelemetry{ - Name: "Naked event", - }, - &MetricTelemetry{ - Name: "my-metric", - Value: 456.0, - }, - &AggregateMetricTelemetry{ - Name: "agg-metric", - Value: 50, - Min: 2, - Max: 7, - Count: 9, - StdDev: 3, - }, - &RequestTelemetry{ - Name: "req-name", - Url: "req-url", - Duration: time.Minute, - ResponseCode: "Response", - Success: true, - Source: "localhost", - }, - &RemoteDependencyTelemetry{ - Name: "dep-name", - ResultCode: "ok.", - Duration: time.Hour, - Success: true, - Data: "dep-data", - Type: "dep-type", - Target: "dep-target", - }, - &AvailabilityTelemetry{ - Name: "avail-name", - Duration: 3 * time.Minute, - Success: true, - RunLocation: "run-loc", - Message: "avail-msg", - }, - &PageViewTelemetry{ - Url: "page-view-url", - Duration: 4 * time.Second, - Name: "page-view-name", - }, - ) - - j, err := parsePayload(buffer.serialize()) - if err != nil { - t.Errorf("Error parsing payload: %s", err.Error()) - } - - if len(j) != 8 { - t.Fatal("Unexpected event count") - } - - // Trace - j[0].assertPath(t, "iKey", test_ikey) - j[0].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Message") - j[0].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[0].assertPath(t, "sampleRate", 100) - j[0].assertPath(t, "data.baseType", "MessageData") - j[0].assertPath(t, "data.baseData.message", "Naked telemetry") - j[0].assertPath(t, "data.baseData.severityLevel", 2) - j[0].assertPath(t, "data.baseData.ver", 2) - - // Event - j[1].assertPath(t, "iKey", test_ikey) - j[1].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Event") - j[1].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[1].assertPath(t, "sampleRate", 100) - j[1].assertPath(t, "data.baseType", "EventData") - j[1].assertPath(t, "data.baseData.name", "Naked event") - j[1].assertPath(t, "data.baseData.ver", 2) - - // Metric - j[2].assertPath(t, "iKey", test_ikey) - j[2].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") - j[2].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[2].assertPath(t, "sampleRate", 100) - j[2].assertPath(t, "data.baseType", "MetricData") - j[2].assertPath(t, "data.baseData.metrics.", 1) - j[2].assertPath(t, "data.baseData.metrics.[0].value", 456) - j[2].assertPath(t, "data.baseData.metrics.[0].count", 1) - j[2].assertPath(t, "data.baseData.metrics.[0].kind", 0) - j[2].assertPath(t, "data.baseData.metrics.[0].name", "my-metric") - j[2].assertPath(t, "data.baseData.ver", 2) - - // Aggregate metric - j[3].assertPath(t, "iKey", test_ikey) - j[3].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Metric") - j[3].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[3].assertPath(t, "sampleRate", 100.0) - j[3].assertPath(t, "data.baseType", "MetricData") - j[3].assertPath(t, "data.baseData.metrics.", 1) - j[3].assertPath(t, "data.baseData.metrics.[0].value", 50) - j[3].assertPath(t, "data.baseData.metrics.[0].count", 9) - j[3].assertPath(t, "data.baseData.metrics.[0].kind", 1) - j[3].assertPath(t, "data.baseData.metrics.[0].min", 2) - j[3].assertPath(t, "data.baseData.metrics.[0].max", 7) - j[3].assertPath(t, "data.baseData.metrics.[0].stdDev", 3) - j[3].assertPath(t, "data.baseData.metrics.[0].name", "agg-metric") - j[3].assertPath(t, "data.baseData.ver", 2) - - // Request - j[4].assertPath(t, "iKey", test_ikey) - j[4].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Request") - j[4].assertPath(t, "time", "2017-11-18T10:35:21Z") // Context takes current time since it's not supplied - j[4].assertPath(t, "sampleRate", 100.0) - j[4].assertPath(t, "data.baseType", "RequestData") - j[4].assertPath(t, "data.baseData.name", "req-name") - j[4].assertPath(t, "data.baseData.duration", "0.00:01:00.0000000") - j[4].assertPath(t, "data.baseData.responseCode", "Response") - j[4].assertPath(t, "data.baseData.success", true) - j[4].assertPath(t, "data.baseData.url", "req-url") - j[4].assertPath(t, "data.baseData.source", "localhost") - j[4].assertPath(t, "data.baseData.ver", 2) - - if id, err := j[4].getPath("data.baseData.id"); err != nil { - t.Errorf("Id not present") - } else if len(id.(string)) == 0 { - t.Errorf("Empty request id") - } - - // Remote dependency - j[5].assertPath(t, "iKey", test_ikey) - j[5].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.RemoteDependency") - j[5].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[5].assertPath(t, "sampleRate", 100.0) - j[5].assertPath(t, "data.baseType", "RemoteDependencyData") - j[5].assertPath(t, "data.baseData.name", "dep-name") - j[5].assertPath(t, "data.baseData.id", "") - j[5].assertPath(t, "data.baseData.resultCode", "ok.") - j[5].assertPath(t, "data.baseData.duration", "0.01:00:00.0000000") - j[5].assertPath(t, "data.baseData.success", true) - j[5].assertPath(t, "data.baseData.data", "dep-data") - j[5].assertPath(t, "data.baseData.target", "dep-target") - j[5].assertPath(t, "data.baseData.type", "dep-type") - j[5].assertPath(t, "data.baseData.ver", 2) - - // Availability - j[6].assertPath(t, "iKey", test_ikey) - j[6].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.Availability") - j[6].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[6].assertPath(t, "sampleRate", 100.0) - j[6].assertPath(t, "data.baseType", "AvailabilityData") - j[6].assertPath(t, "data.baseData.name", "avail-name") - j[6].assertPath(t, "data.baseData.duration", "0.00:03:00.0000000") - j[6].assertPath(t, "data.baseData.success", true) - j[6].assertPath(t, "data.baseData.runLocation", "run-loc") - j[6].assertPath(t, "data.baseData.message", "avail-msg") - j[6].assertPath(t, "data.baseData.id", "") - j[6].assertPath(t, "data.baseData.ver", 2) - - // Page view - j[7].assertPath(t, "iKey", test_ikey) - j[7].assertPath(t, "name", "Microsoft.ApplicationInsights.01234567000089abcdef000000000000.PageView") - j[7].assertPath(t, "time", "2017-11-18T10:35:21Z") - j[7].assertPath(t, "sampleRate", 100.0) - j[7].assertPath(t, "data.baseType", "PageViewData") - j[7].assertPath(t, "data.baseData.name", "page-view-name") - j[7].assertPath(t, "data.baseData.url", "page-view-url") - j[7].assertPath(t, "data.baseData.duration", "0.00:00:04.0000000") - j[7].assertPath(t, "data.baseData.ver", 2) -} - -// Test helpers... - -func telemetryBuffer(items ...Telemetry) telemetryBufferItems { - ctx := NewTelemetryContext(test_ikey) - ctx.iKey = test_ikey - - var result telemetryBufferItems - for _, item := range items { - result = append(result, ctx.envelop(item)) - } - - return result -} - -func (buffer *telemetryBufferItems) add(items ...Telemetry) { - *buffer = append(*buffer, telemetryBuffer(items...)...) -} - -type jsonMessage map[string]interface{} -type jsonPayload []jsonMessage - -func parsePayload(payload []byte) (jsonPayload, error) { - // json.Decoder can detect line endings for us but I'd like to explicitly find them. - var result jsonPayload - for _, item := range bytes.Split(payload, []byte("\n")) { - if len(item) == 0 { - continue - } - - decoder := json.NewDecoder(bytes.NewReader(item)) - msg := make(jsonMessage) - if err := decoder.Decode(&msg); err == nil { - result = append(result, msg) - } else { - return result, err - } - } - - return result, nil -} - -func (msg jsonMessage) assertPath(t *testing.T, path string, value interface{}) { - const tolerance = 0.0001 - v, err := msg.getPath(path) - if err != nil { - t.Error(err.Error()) - return - } - - if num, ok := value.(int); ok { - if vnum, ok := v.(float64); ok { - if math.Abs(float64(num)-vnum) > tolerance { - t.Errorf("Data was unexpected at %s. Got %g want %d", path, vnum, num) - } - } else if vnum, ok := v.(int); ok { - if vnum != num { - t.Errorf("Data was unexpected at %s. Got %d want %d", path, vnum, num) - } - } else { - t.Errorf("Expected value at %s to be a number, but was %T", path, v) - } - } else if num, ok := value.(float64); ok { - if vnum, ok := v.(float64); ok { - if math.Abs(num-vnum) > tolerance { - t.Errorf("Data was unexpected at %s. Got %g want %g", path, vnum, num) - } - } else if vnum, ok := v.(int); ok { - if math.Abs(num-float64(vnum)) > tolerance { - t.Errorf("Data was unexpected at %s. Got %d want %g", path, vnum, num) - } - } else { - t.Errorf("Expected value at %s to be a number, but was %T", path, v) - } - } else if str, ok := value.(string); ok { - if vstr, ok := v.(string); ok { - if str != vstr { - t.Errorf("Data was unexpected at %s. Got '%s' want '%s'", path, vstr, str) - } - } else { - t.Errorf("Expected value at %s to be a string, but was %T", path, v) - } - } else if bl, ok := value.(bool); ok { - if vbool, ok := v.(bool); ok { - if bl != vbool { - t.Errorf("Data was unexpected at %s. Got %t want %t", path, vbool, bl) - } - } else { - t.Errorf("Expected value at %s to be a bool, but was %T", path, v) - } - } else { - t.Errorf("Unsupported type: %#v", value) - } -} - -func (msg jsonMessage) getPath(path string) (interface{}, error) { - parts := strings.Split(path, ".") - var obj interface{} = msg - for i, part := range parts { - if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { - // Array - idxstr := part[1 : len(part)-2] - idx, _ := strconv.Atoi(idxstr) - - if ar, ok := obj.([]interface{}); ok { - if idx >= len(ar) { - return nil, fmt.Errorf("Index out of bounds: %s", strings.Join(parts[0:i+1], ".")) - } - - obj = ar[idx] - } else { - return nil, fmt.Errorf("Path %s is not an array", strings.Join(parts[0:i], ".")) - } - } else if part == "" { - if ar, ok := obj.([]interface{}); ok { - return len(ar), nil - } - } else { - // Map - if dict, ok := obj.(jsonMessage); ok { - if val, ok := dict[part]; ok { - obj = val - } else { - return nil, fmt.Errorf("Key %s not found in %s", part, strings.Join(parts[0:i], ".")) - } - } else if dict, ok := obj.(map[string]interface{}); ok { - if val, ok := dict[part]; ok { - obj = val - } else { - return nil, fmt.Errorf("Key %s not found in %s", part, strings.Join(parts[0:i], ".")) - } - } else { - return nil, fmt.Errorf("Path %s is not a map", strings.Join(parts[0:i], ".")) - } - } - } - - return obj, nil -} diff --git a/application-insights/appinsights/package.go b/application-insights/appinsights/package.go deleted file mode 100644 index 8944a51617..0000000000 --- a/application-insights/appinsights/package.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package appinsights provides an interface to submit telemetry to Application Insights. -// See more at https://azure.microsoft.com/en-us/services/application-insights/ -package appinsights - -const ( - sdkName = "go" - Version = "0.4.4" -) diff --git a/application-insights/appinsights/telemetry.go b/application-insights/appinsights/telemetry.go deleted file mode 100644 index 54b88781e7..0000000000 --- a/application-insights/appinsights/telemetry.go +++ /dev/null @@ -1,652 +0,0 @@ -package appinsights - -import ( - "fmt" - "math" - "net/url" - "strconv" - "time" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -// Common interface implemented by telemetry data contracts -type TelemetryData interface { - EnvelopeName(string) string - BaseType() string - Sanitize() []string -} - -// Common interface implemented by telemetry items that can be passed to -// TelemetryClient.Track -type Telemetry interface { - // Gets the time when this item was measured - Time() time.Time - - // Sets the timestamp to the specified time. - SetTime(time.Time) - - // Gets context data containing extra, optional tags. Overrides - // values found on client TelemetryContext. - ContextTags() map[string]string - - // Gets the data contract as it will be submitted to the data - // collector. - TelemetryData() TelemetryData - - // Gets custom properties to submit with the telemetry item. - GetProperties() map[string]string - - // Gets custom measurements to submit with the telemetry item. - GetMeasurements() map[string]float64 -} - -// BaseTelemetry is the common base struct for telemetry items. -type BaseTelemetry struct { - // The time this when this item was measured - Timestamp time.Time - - // Custom properties - Properties map[string]string - - // Telemetry Context containing extra, optional tags. - Tags contracts.ContextTags -} - -// BaseTelemetryMeasurements provides the Measurements field for telemetry -// items that support it. -type BaseTelemetryMeasurements struct { - // Custom measurements - Measurements map[string]float64 -} - -// BaseTelemetryNoMeasurements provides no Measurements field for telemetry -// items that omit it. -type BaseTelemetryNoMeasurements struct { -} - -// Time returns the timestamp when this was measured. -func (item *BaseTelemetry) Time() time.Time { - return item.Timestamp -} - -// SetTime sets the timestamp to the specified time. -func (item *BaseTelemetry) SetTime(t time.Time) { - item.Timestamp = t -} - -// Gets context data containing extra, optional tags. Overrides values -// found on client TelemetryContext. -func (item *BaseTelemetry) ContextTags() map[string]string { - return item.Tags -} - -// Gets custom properties to submit with the telemetry item. -func (item *BaseTelemetry) GetProperties() map[string]string { - return item.Properties -} - -// Gets custom measurements to submit with the telemetry item. -func (item *BaseTelemetryMeasurements) GetMeasurements() map[string]float64 { - return item.Measurements -} - -// GetMeasurements returns nil for telemetry items that do not support measurements. -func (item *BaseTelemetryNoMeasurements) GetMeasurements() map[string]float64 { - return nil -} - -// Trace telemetry items represent printf-like trace statements that can be -// text searched. -type TraceTelemetry struct { - BaseTelemetry - BaseTelemetryNoMeasurements - - // Trace message - Message string - - // Severity level - SeverityLevel contracts.SeverityLevel -} - -// Creates a trace telemetry item with the specified message and severity -// level. -func NewTraceTelemetry(message string, severityLevel contracts.SeverityLevel) *TraceTelemetry { - return &TraceTelemetry{ - Message: message, - SeverityLevel: severityLevel, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - } -} - -func (trace *TraceTelemetry) TelemetryData() TelemetryData { - data := contracts.NewMessageData() - data.Message = trace.Message - data.Properties = trace.Properties - data.SeverityLevel = trace.SeverityLevel - - return data -} - -// Event telemetry items represent structured event records. -type EventTelemetry struct { - BaseTelemetry - BaseTelemetryMeasurements - - // Event name - Name string -} - -// Creates an event telemetry item with the specified name. -func NewEventTelemetry(name string) *EventTelemetry { - return &EventTelemetry{ - Name: name, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - BaseTelemetryMeasurements: BaseTelemetryMeasurements{ - Measurements: make(map[string]float64), - }, - } -} - -func (event *EventTelemetry) TelemetryData() TelemetryData { - data := contracts.NewEventData() - data.Name = event.Name - data.Properties = event.Properties - data.Measurements = event.Measurements - - return data -} - -// Metric telemetry items each represent a single data point. -type MetricTelemetry struct { - BaseTelemetry - BaseTelemetryNoMeasurements - - // Metric name - Name string - - // Sampled value - Value float64 -} - -// Creates a metric telemetry sample with the specified name and value. -func NewMetricTelemetry(name string, value float64) *MetricTelemetry { - return &MetricTelemetry{ - Name: name, - Value: value, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - } -} - -func (metric *MetricTelemetry) TelemetryData() TelemetryData { - dataPoint := contracts.NewDataPoint() - dataPoint.Name = metric.Name - dataPoint.Value = metric.Value - dataPoint.Count = 1 - dataPoint.Kind = contracts.Measurement - - data := contracts.NewMetricData() - data.Metrics = []*contracts.DataPoint{dataPoint} - data.Properties = metric.Properties - - return data -} - -// Aggregated metric telemetry items represent an aggregation of data points -// over time. These values can be calculated by the caller or with the AddData -// function. -type AggregateMetricTelemetry struct { - BaseTelemetry - BaseTelemetryNoMeasurements - - // Metric name - Name string - - // Sum of individual measurements - Value float64 - - // Minimum value of the aggregated metric - Min float64 - - // Maximum value of the aggregated metric - Max float64 - - // Count of measurements in the sample - Count int - - // Standard deviation of the aggregated metric - StdDev float64 - - // Variance of the aggregated metric. As an invariant, - // either this or the StdDev should be zero at any given time. - // If both are non-zero then StdDev takes precedence. - Variance float64 -} - -// Creates a new aggregated metric telemetry item with the specified name. -// Values should be set on the object returned before submission. -func NewAggregateMetricTelemetry(name string) *AggregateMetricTelemetry { - return &AggregateMetricTelemetry{ - Name: name, - Count: 0, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - } -} - -// Adds data points to the aggregate totals included in this telemetry item. -// This can be used for all the data at once or incrementally. Calculates -// Min, Max, Sum, Count, and StdDev (by way of Variance). -func (agg *AggregateMetricTelemetry) AddData(values []float64) { - if agg.StdDev != 0.0 { - // If StdDev is non-zero, then square it to produce - // the variance, which is better for incremental calculations, - // and then zero it out. - agg.Variance = agg.StdDev * agg.StdDev - agg.StdDev = 0.0 - } - - vsum := agg.addData(values, agg.Variance*float64(agg.Count)) - if agg.Count > 0 { - agg.Variance = vsum / float64(agg.Count) - } -} - -// Adds sampled data points to the aggregate totals included in this telemetry item. -// This can be used for all the data at once or incrementally. Differs from AddData -// in how it calculates standard deviation, and should not be used interchangeably -// with AddData. -func (agg *AggregateMetricTelemetry) AddSampledData(values []float64) { - if agg.StdDev != 0.0 { - // If StdDev is non-zero, then square it to produce - // the variance, which is better for incremental calculations, - // and then zero it out. - agg.Variance = agg.StdDev * agg.StdDev - agg.StdDev = 0.0 - } - - vsum := agg.addData(values, agg.Variance*float64(agg.Count-1)) - if agg.Count > 1 { - // Sampled values should divide by n-1 - agg.Variance = vsum / float64(agg.Count-1) - } -} - -func (agg *AggregateMetricTelemetry) addData(values []float64, vsum float64) float64 { - if len(values) == 0 { - return vsum - } - - // Running tally of the mean is important for incremental variance computation. - var mean float64 - - if agg.Count == 0 { - agg.Min = values[0] - agg.Max = values[0] - } else { - mean = agg.Value / float64(agg.Count) - } - - for _, x := range values { - // Update Min, Max, Count, and Value - agg.Count++ - agg.Value += x - - if x < agg.Min { - agg.Min = x - } - - if x > agg.Max { - agg.Max = x - } - - // Welford's algorithm to compute variance. The divide occurs in the caller. - newMean := agg.Value / float64(agg.Count) - vsum += (x - mean) * (x - newMean) - mean = newMean - } - - return vsum -} - -func (agg *AggregateMetricTelemetry) TelemetryData() TelemetryData { - dataPoint := contracts.NewDataPoint() - dataPoint.Name = agg.Name - dataPoint.Value = agg.Value - dataPoint.Kind = contracts.Aggregation - dataPoint.Min = agg.Min - dataPoint.Max = agg.Max - dataPoint.Count = agg.Count - - if agg.StdDev != 0.0 { - dataPoint.StdDev = agg.StdDev - } else if agg.Variance > 0.0 { - dataPoint.StdDev = math.Sqrt(agg.Variance) - } - - data := contracts.NewMetricData() - data.Metrics = []*contracts.DataPoint{dataPoint} - data.Properties = agg.Properties - - return data -} - -// Request telemetry items represents completion of an external request to the -// application and contains a summary of that request execution and results. -type RequestTelemetry struct { - BaseTelemetry - BaseTelemetryMeasurements - - // Identifier of a request call instance. Used for correlation between request - // and other telemetry items. - Id string - - // Request name. For HTTP requests it represents the HTTP method and URL path template. - Name string - - // URL of the request with all query string parameters. - Url string - - // Duration to serve the request. - Duration time.Duration - - // Results of a request execution. HTTP status code for HTTP requests. - ResponseCode string - - // Indication of successful or unsuccessful call. - Success bool - - // Source of the request. Examplese are the instrumentation key of the caller - // or the ip address of the caller. - Source string -} - -// Creates a new request telemetry item for HTTP requests. The success value will be -// computed from responseCode, and the timestamp will be set to the current time minus -// the duration. -func NewRequestTelemetry(method, uri string, duration time.Duration, responseCode string) *RequestTelemetry { - success := true - code, err := strconv.Atoi(responseCode) - if err == nil { - success = code < 400 || code == 401 - } - - nameUri := uri - - // Sanitize URL for the request name - if parsedUrl, err := url.Parse(uri); err == nil { - // Remove the query - parsedUrl.RawQuery = "" - parsedUrl.ForceQuery = false - - // Remove the fragment - parsedUrl.Fragment = "" - - // Remove the user info, if any. - parsedUrl.User = nil - - // Write back to name - nameUri = parsedUrl.String() - } - - return &RequestTelemetry{ - Name: fmt.Sprintf("%s %s", method, nameUri), - Url: uri, - Id: newUUID().String(), - Duration: duration, - ResponseCode: responseCode, - Success: success, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now().Add(-duration), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - BaseTelemetryMeasurements: BaseTelemetryMeasurements{ - Measurements: make(map[string]float64), - }, - } -} - -// Sets the timestamp and duration of this telemetry item based on the provided -// start and end times. -func (request *RequestTelemetry) MarkTime(startTime, endTime time.Time) { - request.Timestamp = startTime - request.Duration = endTime.Sub(startTime) -} - -func (request *RequestTelemetry) TelemetryData() TelemetryData { - data := contracts.NewRequestData() - data.Name = request.Name - data.Duration = formatDuration(request.Duration) - data.ResponseCode = request.ResponseCode - data.Success = request.Success - data.Url = request.Url - data.Source = request.Source - - if request.Id == "" { - data.Id = newUUID().String() - } else { - data.Id = request.Id - } - - data.Properties = request.Properties - data.Measurements = request.Measurements - return data -} - -// Remote dependency telemetry items represent interactions of the monitored -// component with a remote component/service like SQL or an HTTP endpoint. -type RemoteDependencyTelemetry struct { - BaseTelemetry - BaseTelemetryMeasurements - - // Name of the command that initiated this dependency call. Low cardinality - // value. Examples are stored procedure name and URL path template. - Name string - - // Identifier of a dependency call instance. Used for correlation with the - // request telemetry item corresponding to this dependency call. - Id string - - // Result code of a dependency call. Examples are SQL error code and HTTP - // status code. - ResultCode string - - // Duration of the remote call. - Duration time.Duration - - // Indication of successful or unsuccessful call. - Success bool - - // Command initiated by this dependency call. Examples are SQL statement and - // HTTP URL's with all the query parameters. - Data string - - // Dependency type name. Very low cardinality. Examples are SQL, Azure table, - // and HTTP. - Type string - - // Target site of a dependency call. Examples are server name, host address. - Target string -} - -// Builds a new Remote Dependency telemetry item, with the specified name, -// dependency type, target site, and success status. -func NewRemoteDependencyTelemetry(name, dependencyType, target string, success bool) *RemoteDependencyTelemetry { - return &RemoteDependencyTelemetry{ - Name: name, - Type: dependencyType, - Target: target, - Success: success, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - BaseTelemetryMeasurements: BaseTelemetryMeasurements{ - Measurements: make(map[string]float64), - }, - } -} - -// Sets the timestamp and duration of this telemetry item based on the provided -// start and end times. -func (telem *RemoteDependencyTelemetry) MarkTime(startTime, endTime time.Time) { - telem.Timestamp = startTime - telem.Duration = endTime.Sub(startTime) -} - -func (telem *RemoteDependencyTelemetry) TelemetryData() TelemetryData { - data := contracts.NewRemoteDependencyData() - data.Name = telem.Name - data.Id = telem.Id - data.ResultCode = telem.ResultCode - data.Duration = formatDuration(telem.Duration) - data.Success = telem.Success - data.Data = telem.Data - data.Target = telem.Target - data.Properties = telem.Properties - data.Measurements = telem.Measurements - data.Type = telem.Type - - return data -} - -// Avaibility telemetry items represent the result of executing an availability -// test. -type AvailabilityTelemetry struct { - BaseTelemetry - BaseTelemetryMeasurements - - // Identifier of a test run. Used to correlate steps of test run and - // telemetry generated by the service. - Id string - - // Name of the test that this result represents. - Name string - - // Duration of the test run. - Duration time.Duration - - // Success flag. - Success bool - - // Name of the location where the test was run. - RunLocation string - - // Diagnostic message for the result. - Message string -} - -// Creates a new availability telemetry item with the specified test name, -// duration and success code. -func NewAvailabilityTelemetry(name string, duration time.Duration, success bool) *AvailabilityTelemetry { - return &AvailabilityTelemetry{ - Name: name, - Duration: duration, - Success: success, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - BaseTelemetryMeasurements: BaseTelemetryMeasurements{ - Measurements: make(map[string]float64), - }, - } -} - -// Sets the timestamp and duration of this telemetry item based on the provided -// start and end times. -func (telem *AvailabilityTelemetry) MarkTime(startTime, endTime time.Time) { - telem.Timestamp = startTime - telem.Duration = endTime.Sub(startTime) -} - -func (telem *AvailabilityTelemetry) TelemetryData() TelemetryData { - data := contracts.NewAvailabilityData() - data.Name = telem.Name - data.Duration = formatDuration(telem.Duration) - data.Success = telem.Success - data.RunLocation = telem.RunLocation - data.Message = telem.Message - data.Properties = telem.Properties - data.Id = telem.Id - data.Measurements = telem.Measurements - - return data -} - -// Page view telemetry items represent generic actions on a page like a button -// click. -type PageViewTelemetry struct { - BaseTelemetry - BaseTelemetryMeasurements - - // Request URL with all query string parameters - Url string - - // Request duration. - Duration time.Duration - - // Event name. - Name string -} - -// Creates a new page view telemetry item with the specified name and url. -func NewPageViewTelemetry(name, url string) *PageViewTelemetry { - return &PageViewTelemetry{ - Name: name, - Url: url, - BaseTelemetry: BaseTelemetry{ - Timestamp: currentClock.Now(), - Tags: make(contracts.ContextTags), - Properties: make(map[string]string), - }, - BaseTelemetryMeasurements: BaseTelemetryMeasurements{ - Measurements: make(map[string]float64), - }, - } -} - -// Sets the timestamp and duration of this telemetry item based on the provided -// start and end times. -func (telem *PageViewTelemetry) MarkTime(startTime, endTime time.Time) { - telem.Timestamp = startTime - telem.Duration = endTime.Sub(startTime) -} - -func (telem *PageViewTelemetry) TelemetryData() TelemetryData { - data := contracts.NewPageViewData() - data.Url = telem.Url - data.Duration = formatDuration(telem.Duration) - data.Name = telem.Name - data.Properties = telem.Properties - data.Measurements = telem.Measurements - return data -} - -func formatDuration(d time.Duration) string { - ticks := int64(d/(time.Nanosecond*100)) % 10000000 - seconds := int64(d/time.Second) % 60 - minutes := int64(d/time.Minute) % 60 - hours := int64(d/time.Hour) % 24 - days := int64(d / (time.Hour * 24)) - - return fmt.Sprintf("%d.%02d:%02d:%02d.%07d", days, hours, minutes, seconds, ticks) -} diff --git a/application-insights/appinsights/telemetry_test.go b/application-insights/appinsights/telemetry_test.go deleted file mode 100644 index ba66508180..0000000000 --- a/application-insights/appinsights/telemetry_test.go +++ /dev/null @@ -1,368 +0,0 @@ -package appinsights - -import ( - "fmt" - "math" - "testing" - "time" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -const float_precision = 1e-4 - -func checkDataContract(t *testing.T, property string, actual, expected interface{}) { - if x, ok := actual.(float64); ok { - if y, ok := expected.(float64); ok { - if math.Abs(x-y) > float_precision { - t.Errorf("Float property %s mismatched; got %f, want %f.\n", property, actual, expected) - } - - return - } - } - - if actual != expected { - t.Errorf("Property %s mismatched; got %v, want %v.\n", property, actual, expected) - } -} - -func checkNotNullOrEmpty(t *testing.T, property string, actual interface{}) { - if actual == nil { - t.Errorf("Property %s was expected not to be null.\n", property) - } else if str, ok := actual.(string); ok && str == "" { - t.Errorf("Property %s was expected not to be an empty string.\n", property) - } -} - -func TestTraceTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewTraceTelemetry("~my message~", Error) - telem.Properties["prop1"] = "value1" - telem.Properties["prop2"] = "value2" - d := telem.TelemetryData().(*contracts.MessageData) - - checkDataContract(t, "Message", d.Message, "~my message~") - checkDataContract(t, "SeverityLevel", d.SeverityLevel, Error) - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") - checkDataContract(t, "Properties[prop2]", d.Properties["prop2"], "value2") - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - telem2 := &TraceTelemetry{ - Message: "~my-2nd-message~", - SeverityLevel: Critical, - } - d2 := telem2.TelemetryData().(*contracts.MessageData) - - checkDataContract(t, "Message", d2.Message, "~my-2nd-message~") - checkDataContract(t, "SeverityLevel", d2.SeverityLevel, Critical) - - var telemInterface Telemetry - if telemInterface = telem; telemInterface.GetMeasurements() != nil { - t.Errorf("Trace.(Telemetry).GetMeasurements should return nil") - } -} - -func TestEventTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewEventTelemetry("~my event~") - telem.Properties["prop1"] = "value1" - telem.Properties["prop2"] = "value2" - telem.Measurements["measure1"] = 1234.0 - telem.Measurements["measure2"] = 5678.0 - d := telem.TelemetryData().(*contracts.EventData) - - checkDataContract(t, "Name", d.Name, "~my event~") - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") - checkDataContract(t, "Properties[prop2]", d.Properties["prop2"], "value2") - checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 1234.0) - checkDataContract(t, "Measurements[measure2]", d.Measurements["measure2"], 5678.0) - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - telem2 := &EventTelemetry{ - Name: "~my-event~", - } - d2 := telem2.TelemetryData().(*contracts.EventData) - - checkDataContract(t, "Name", d2.Name, "~my-event~") -} - -func TestMetricTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewMetricTelemetry("~my metric~", 1234.0) - telem.Properties["prop1"] = "value!" - d := telem.TelemetryData().(*contracts.MetricData) - - checkDataContract(t, "len(Metrics)", len(d.Metrics), 1) - dp := d.Metrics[0] - checkDataContract(t, "DataPoint.Name", dp.Name, "~my metric~") - checkDataContract(t, "DataPoint.Value", dp.Value, 1234.0) - checkDataContract(t, "DataPoint.Kind", dp.Kind, Measurement) - checkDataContract(t, "DataPoint.Count", dp.Count, 1) - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value!") - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - telem2 := &MetricTelemetry{ - Name: "~my metric~", - Value: 5678.0, - } - d2 := telem2.TelemetryData().(*contracts.MetricData) - - checkDataContract(t, "len(Metrics)", len(d2.Metrics), 1) - dp2 := d2.Metrics[0] - checkDataContract(t, "DataPoint.Name", dp2.Name, "~my metric~") - checkDataContract(t, "DataPoint.Value", dp2.Value, 5678.0) - checkDataContract(t, "DataPoint.Kind", dp2.Kind, Measurement) - checkDataContract(t, "DataPoint.Count", dp2.Count, 1) - - var telemInterface Telemetry - if telemInterface = telem; telemInterface.GetMeasurements() != nil { - t.Errorf("Metric.(Telemetry).GetMeasurements should return nil") - } -} - -type statsTest struct { - data []float64 - stdDev float64 - sampledStdDev float64 - min float64 - max float64 -} - -func TestAggregateMetricTelemetry(t *testing.T) { - statsTests := []statsTest{ - statsTest{[]float64{}, 0.0, 0.0, 0.0, 0.0}, - statsTest{[]float64{0.0}, 0.0, 0.0, 0.0, 0.0}, - statsTest{[]float64{50.0}, 0.0, 0.0, 50.0, 50.0}, - statsTest{[]float64{50.0, 50.0}, 0.0, 0.0, 50.0, 50.0}, - statsTest{[]float64{50.0, 60.0}, 5.0, 7.071, 50.0, 60.0}, - statsTest{[]float64{9.0, 10.0, 11.0, 7.0, 13.0}, 2.0, 2.236, 7.0, 13.0}, - // TODO: More tests. - } - - for _, tst := range statsTests { - t1 := NewAggregateMetricTelemetry("foo") - t2 := NewAggregateMetricTelemetry("foo") - t1.AddData(tst.data) - t2.AddSampledData(tst.data) - - checkDataPoint(t, t1, tst, false) - checkDataPoint(t, t2, tst, true) - } - - // Do the same as above, but add data points one at a time. - for _, tst := range statsTests { - t1 := NewAggregateMetricTelemetry("foo") - t2 := NewAggregateMetricTelemetry("foo") - - for _, x := range tst.data { - t1.AddData([]float64{x}) - t2.AddSampledData([]float64{x}) - } - - checkDataPoint(t, t1, tst, false) - checkDataPoint(t, t2, tst, true) - } -} - -func checkDataPoint(t *testing.T, telem *AggregateMetricTelemetry, tst statsTest, sampled bool) { - d := telem.TelemetryData().(*contracts.MetricData) - checkDataContract(t, "len(Metrics)", len(d.Metrics), 1) - dp := d.Metrics[0] - - var sum float64 - for _, x := range tst.data { - sum += x - } - - checkDataContract(t, "DataPoint.Count", dp.Count, len(tst.data)) - checkDataContract(t, "DataPoint.Min", dp.Min, tst.min) - checkDataContract(t, "DataPoint.Max", dp.Max, tst.max) - checkDataContract(t, "DataPoint.Value", dp.Value, sum) - - if sampled { - checkDataContract(t, "DataPoint.StdDev (sample)", dp.StdDev, tst.sampledStdDev) - } else { - checkDataContract(t, "DataPoint.StdDev (population)", dp.StdDev, tst.stdDev) - } -} - -func TestRequestTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewRequestTelemetry("POST", "http://testurl.org/?query=value", time.Minute, "200") - telem.Source = "127.0.0.1" - telem.Properties["prop1"] = "value1" - telem.Measurements["measure1"] = 999.0 - d := telem.TelemetryData().(*contracts.RequestData) - - checkNotNullOrEmpty(t, "Id", d.Id) - checkDataContract(t, "Name", d.Name, "POST http://testurl.org/") - checkDataContract(t, "Url", d.Url, "http://testurl.org/?query=value") - checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") - checkDataContract(t, "Success", d.Success, true) - checkDataContract(t, "Source", d.Source, "127.0.0.1") - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") - checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now().Add(-time.Minute)) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - startTime := currentClock.Now().Add(-time.Hour) - endTime := startTime.Add(5 * time.Minute) - telem.MarkTime(startTime, endTime) - d = telem.TelemetryData().(*contracts.RequestData) - checkDataContract(t, "Timestamp", telem.Time(), startTime) - checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") -} - -func TestRequestTelemetrySuccess(t *testing.T) { - // Some of these are due to default-success - successCodes := []string{"200", "204", "301", "302", "401", "foo", "", "55555555555555555555555555555555555555555555555555"} - failureCodes := []string{"400", "404", "500", "430"} - - for _, code := range successCodes { - telem := NewRequestTelemetry("GET", "https://something", time.Second, code) - d := telem.TelemetryData().(*contracts.RequestData) - checkDataContract(t, fmt.Sprintf("Success [%s]", code), d.Success, true) - } - - for _, code := range failureCodes { - telem := NewRequestTelemetry("GET", "https://something", time.Second, code) - d := telem.TelemetryData().(*contracts.RequestData) - checkDataContract(t, fmt.Sprintf("Success [%s]", code), d.Success, false) - } -} - -func TestRemoteDependencyTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewRemoteDependencyTelemetry("SQL-GET", "SQL", "myhost.name", true) - telem.Data = "" - telem.ResultCode = "OK" - telem.Duration = time.Minute - telem.Properties["prop1"] = "value1" - telem.Measurements["measure1"] = 999.0 - d := telem.TelemetryData().(*contracts.RemoteDependencyData) - - checkDataContract(t, "Id", d.Id, "") // no default - checkDataContract(t, "Data", d.Data, "") - checkDataContract(t, "Type", d.Type, "SQL") - checkDataContract(t, "Target", d.Target, "myhost.name") - checkDataContract(t, "ResultCode", d.ResultCode, "OK") - checkDataContract(t, "Name", d.Name, "SQL-GET") - checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") - checkDataContract(t, "Success", d.Success, true) - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") - checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - telem.Id = "" - telem.Success = false - d = telem.TelemetryData().(*contracts.RemoteDependencyData) - checkDataContract(t, "Id", d.Id, "") - checkDataContract(t, "Success", d.Success, false) - - startTime := currentClock.Now().Add(-time.Hour) - endTime := startTime.Add(5 * time.Minute) - telem.MarkTime(startTime, endTime) - d = telem.TelemetryData().(*contracts.RemoteDependencyData) - checkDataContract(t, "Timestamp", telem.Time(), startTime) - checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") -} - -func TestAvailabilityTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewAvailabilityTelemetry("Frontdoor", time.Minute, true) - telem.RunLocation = "The moon" - telem.Message = "OK" - telem.Properties["prop1"] = "value1" - telem.Measurements["measure1"] = 999.0 - d := telem.TelemetryData().(*contracts.AvailabilityData) - - checkDataContract(t, "Id", d.Id, "") - checkDataContract(t, "Name", d.Name, "Frontdoor") - checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") - checkDataContract(t, "RunLocation", d.RunLocation, "The moon") - checkDataContract(t, "Message", d.Message, "OK") - checkDataContract(t, "Success", d.Success, true) - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") - checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - telem.Id = "" - telem.Success = false - d = telem.TelemetryData().(*contracts.AvailabilityData) - checkDataContract(t, "Id", d.Id, "") - checkDataContract(t, "Success", d.Success, false) - - startTime := currentClock.Now().Add(-time.Hour) - endTime := startTime.Add(5 * time.Minute) - telem.MarkTime(startTime, endTime) - d = telem.TelemetryData().(*contracts.AvailabilityData) - checkDataContract(t, "Timestamp", telem.Time(), startTime) - checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") -} - -func TestPageViewTelemetry(t *testing.T) { - mockClock() - defer resetClock() - - telem := NewPageViewTelemetry("Home page", "http://testuri.org/") - telem.Duration = time.Minute - telem.Properties["prop1"] = "value1" - telem.Measurements["measure1"] = 999.0 - d := telem.TelemetryData().(*contracts.PageViewData) - - checkDataContract(t, "Name", d.Name, "Home page") - checkDataContract(t, "Duration", d.Duration, "0.00:01:00.0000000") - checkDataContract(t, "Url", d.Url, "http://testuri.org/") - checkDataContract(t, "Properties[prop1]", d.Properties["prop1"], "value1") - checkDataContract(t, "Measurements[measure1]", d.Measurements["measure1"], 999.0) - checkDataContract(t, "Timestamp", telem.Time(), currentClock.Now()) - checkNotNullOrEmpty(t, "ContextTags", telem.ContextTags()) - - startTime := currentClock.Now().Add(-time.Hour) - endTime := startTime.Add(5 * time.Minute) - telem.MarkTime(startTime, endTime) - d = telem.TelemetryData().(*contracts.PageViewData) - checkDataContract(t, "Timestamp", telem.Time(), startTime) - checkDataContract(t, "Duration", d.Duration, "0.00:05:00.0000000") -} - -type durationTest struct { - duration time.Duration - expected string -} - -func TestFormatDuration(t *testing.T) { - durationTests := []durationTest{ - durationTest{time.Hour, "0.01:00:00.0000000"}, - durationTest{time.Minute, "0.00:01:00.0000000"}, - durationTest{time.Second, "0.00:00:01.0000000"}, - durationTest{time.Millisecond, "0.00:00:00.0010000"}, - durationTest{100 * time.Nanosecond, "0.00:00:00.0000001"}, - durationTest{(31 * time.Hour) + (25 * time.Minute) + (30 * time.Second) + time.Millisecond, "1.07:25:30.0010000"}, - } - - for _, tst := range durationTests { - actual := formatDuration(tst.duration) - if tst.expected != actual { - t.Errorf("Mismatch. Got %s, want %s (duration %s)\n", actual, tst.expected, tst.duration.String()) - } - } -} diff --git a/application-insights/appinsights/telemetrychannel.go b/application-insights/appinsights/telemetrychannel.go deleted file mode 100644 index 189b3cab82..0000000000 --- a/application-insights/appinsights/telemetrychannel.go +++ /dev/null @@ -1,51 +0,0 @@ -package appinsights - -import ( - "time" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -// Implementations of TelemetryChannel are responsible for queueing and -// periodically submitting telemetry items. -type TelemetryChannel interface { - // The address of the endpoint to which telemetry is sent - EndpointAddress() string - - // Queues a single telemetry item - Send(*contracts.Envelope) - - // Forces the current queue to be sent - Flush() - - // Tears down the submission goroutines, closes internal channels. - // Any telemetry waiting to be sent is discarded. Further calls to - // Send() have undefined behavior. This is a more abrupt version of - // Close(). - Stop() - - // Returns true if this channel has been throttled by the data - // collector. - IsThrottled() bool - - // Flushes and tears down the submission goroutine and closes - // internal channels. Returns a channel that is closed when all - // pending telemetry items have been submitted and it is safe to - // shut down without losing telemetry. - // - // If retryTimeout is specified and non-zero, then failed - // submissions will be retried until one succeeds or the timeout - // expires, whichever occurs first. A retryTimeout of zero - // indicates that failed submissions will be retried as usual. An - // omitted retryTimeout indicates that submissions should not be - // retried if they fail. - // - // Note that the returned channel may not be closed before - // retryTimeout even if it is specified. This is because - // retryTimeout only applies to the latest telemetry buffer. This - // may be typical for applications that submit a large amount of - // telemetry or are prone to being throttled. When exiting, you - // should select on the result channel and your own timer to avoid - // long delays. - Close(retryTimeout ...time.Duration) <-chan struct{} -} diff --git a/application-insights/appinsights/telemetrycontext.go b/application-insights/appinsights/telemetrycontext.go deleted file mode 100644 index f54e36d146..0000000000 --- a/application-insights/appinsights/telemetrycontext.go +++ /dev/null @@ -1,104 +0,0 @@ -package appinsights - -import ( - "strings" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -// Encapsulates contextual data common to all telemetry submitted through a -// TelemetryClient instance such as including instrumentation key, tags, and -// common properties. -type TelemetryContext struct { - // Instrumentation key - iKey string - - // Stripped-down instrumentation key used in envelope name - nameIKey string - - // Collection of tag data to attach to the telemetry item. - Tags contracts.ContextTags - - // Common properties to add to each telemetry item. This only has - // an effect from the TelemetryClient's context instance. This will - // be nil on telemetry items. - CommonProperties map[string]string -} - -// Creates a new, empty TelemetryContext -func NewTelemetryContext(ikey string) *TelemetryContext { - return &TelemetryContext{ - iKey: ikey, - nameIKey: strings.Replace(ikey, "-", "", -1), - Tags: make(contracts.ContextTags), - CommonProperties: make(map[string]string), - } -} - -// Gets the instrumentation key associated with this TelemetryContext. This -// will be an empty string on telemetry items' context instances. -func (context *TelemetryContext) InstrumentationKey() string { - return context.iKey -} - -// Wraps a telemetry item in an envelope with the information found in this -// context. -func (context *TelemetryContext) envelop(item Telemetry) *contracts.Envelope { - // Apply common properties - if props := item.GetProperties(); props != nil && context.CommonProperties != nil { - for k, v := range context.CommonProperties { - if _, ok := props[k]; !ok { - props[k] = v - } - } - } - - tdata := item.TelemetryData() - data := contracts.NewData() - data.BaseType = tdata.BaseType() - data.BaseData = tdata - - envelope := contracts.NewEnvelope() - envelope.Name = tdata.EnvelopeName(context.nameIKey) - envelope.Data = data - envelope.IKey = context.iKey - - timestamp := item.Time() - if timestamp.IsZero() { - timestamp = currentClock.Now() - } - - envelope.Time = timestamp.UTC().Format("2006-01-02T15:04:05.999999Z") - - if contextTags := item.ContextTags(); contextTags != nil { - envelope.Tags = contextTags - - // Copy in default tag values. - for tagkey, tagval := range context.Tags { - if _, ok := contextTags[tagkey]; !ok { - contextTags[tagkey] = tagval - } - } - } else { - // Create new tags object - envelope.Tags = make(map[string]string) - for k, v := range context.Tags { - envelope.Tags[k] = v - } - } - - // Create operation ID if it does not exist - if _, ok := envelope.Tags[contracts.OperationId]; !ok { - envelope.Tags[contracts.OperationId] = newUUID().String() - } - - // Sanitize. - for _, warn := range tdata.Sanitize() { - diagnosticsWriter.Printf("Telemetry data warning: %s", warn) - } - for _, warn := range contracts.SanitizeTags(envelope.Tags) { - diagnosticsWriter.Printf("Telemetry tag warning: %s", warn) - } - - return envelope -} diff --git a/application-insights/appinsights/telemetrycontext_test.go b/application-insights/appinsights/telemetrycontext_test.go deleted file mode 100644 index ada1a2e2d3..0000000000 --- a/application-insights/appinsights/telemetrycontext_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package appinsights - -import ( - "strings" - "testing" - "time" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -func TestDefaultTags(t *testing.T) { - context := NewTelemetryContext(test_ikey) - context.Tags["test"] = "OK" - context.Tags["no-write"] = "Fail" - - telem := NewTraceTelemetry("Hello world.", Verbose) - telem.Tags["no-write"] = "OK" - - envelope := context.envelop(telem) - - if envelope.Tags["test"] != "OK" { - t.Error("Default client tags did not propagate to telemetry") - } - - if envelope.Tags["no-write"] != "OK" { - t.Error("Default client tag overwrote telemetry item tag") - } -} - -func TestCommonProperties(t *testing.T) { - context := NewTelemetryContext(test_ikey) - context.CommonProperties = map[string]string{ - "test": "OK", - "no-write": "Fail", - } - - telem := NewTraceTelemetry("Hello world.", Verbose) - telem.Properties["no-write"] = "OK" - - envelope := context.envelop(telem) - data := envelope.Data.(*contracts.Data).BaseData.(*contracts.MessageData) - - if data.Properties["test"] != "OK" { - t.Error("Common properties did not propagate to telemetry") - } - - if data.Properties["no-write"] != "OK" { - t.Error("Common properties overwrote telemetry properties") - } -} - -func TestContextTags(t *testing.T) { - // Just a quick test to make sure it works. - tags := make(contracts.ContextTags) - if v := tags.Session().GetId(); v != "" { - t.Error("Failed to get empty session ID") - } - - tags.Session().SetIsFirst("true") - if v := tags.Session().GetIsFirst(); v != "true" { - t.Error("Failed to get value") - } - - if v, ok := tags["ai.session.isFirst"]; !ok || v != "true" { - t.Error("Failed to get isFirst through raw map") - } - - tags.Session().SetIsFirst("") - if v, ok := tags["ai.session.isFirst"]; ok || v != "" { - t.Error("SetIsFirst with empty string failed to remove it from the map") - } -} - -func TestSanitize(t *testing.T) { - name := strings.Repeat("Z", 1024) - val := strings.Repeat("Y", 10240) - - ev := NewEventTelemetry(name) - ev.Properties[name] = val - ev.Measurements[name] = 55.0 - - ctx := NewTelemetryContext(test_ikey) - ctx.Tags.Session().SetId(name) - - // We'll be looking for messages with these values: - found := map[string]int{ - "EventData.Name exceeded": 0, - "EventData.Properties has value": 0, - "EventData.Properties has key": 0, - "EventData.Measurements has key": 0, - "ai.session.id exceeded": 0, - } - - // Set up listener for the warnings. - NewDiagnosticsMessageListener(func(msg string) error { - for k, _ := range found { - if strings.Contains(msg, k) { - found[k] = found[k] + 1 - break - } - } - - return nil - }) - - defer resetDiagnosticsListeners() - - // This may break due to hardcoded limits... Check contracts. - envelope := ctx.envelop(ev) - - // Make sure all the warnings were found in the output - for k, v := range found { - if v != 1 { - t.Errorf("Did not find a warning containing \"%s\"", k) - } - } - - // Check the format of the stuff we found in the envelope - if v, ok := envelope.Tags[contracts.SessionId]; !ok || v != name[:64] { - t.Error("Session ID tag was not truncated") - } - - evdata := envelope.Data.(*contracts.Data).BaseData.(*contracts.EventData) - if evdata.Name != name[:512] { - t.Error("Event name was not truncated") - } - - if v, ok := evdata.Properties[name[:150]]; !ok || v != val[:8192] { - t.Error("Event property name/value was not truncated") - } - - if v, ok := evdata.Measurements[name[:150]]; !ok || v != 55.0 { - t.Error("Event measurement name was not truncated") - } -} - -func TestTimestamp(t *testing.T) { - ev := NewEventTelemetry("event") - ev.Timestamp = time.Unix(1523667421, 500000000) - - envelope := NewTelemetryContext(test_ikey).envelop(ev) - if envelope.Time != "2018-04-14T00:57:01.5Z" { - t.Errorf("Unexpected timestamp: %s", envelope.Time) - } -} diff --git a/application-insights/appinsights/throttle.go b/application-insights/appinsights/throttle.go deleted file mode 100644 index 2c85800d14..0000000000 --- a/application-insights/appinsights/throttle.go +++ /dev/null @@ -1,144 +0,0 @@ -package appinsights - -import ( - "time" -) - -type throttleManager struct { - msgs chan *throttleMessage -} - -type throttleMessage struct { - query bool - wait bool - throttle bool - stop bool - timestamp time.Time - result chan bool -} - -func newThrottleManager() *throttleManager { - result := &throttleManager{ - msgs: make(chan *throttleMessage), - } - - go result.run() - return result -} - -func (throttle *throttleManager) RetryAfter(t time.Time) { - throttle.msgs <- &throttleMessage{ - throttle: true, - timestamp: t, - } -} - -func (throttle *throttleManager) IsThrottled() bool { - ch := make(chan bool) - throttle.msgs <- &throttleMessage{ - query: true, - result: ch, - } - - result := <-ch - close(ch) - return result -} - -func (throttle *throttleManager) NotifyWhenReady() chan bool { - result := make(chan bool, 1) - throttle.msgs <- &throttleMessage{ - wait: true, - result: result, - } - - return result -} - -func (throttle *throttleManager) Stop() { - result := make(chan bool) - throttle.msgs <- &throttleMessage{ - stop: true, - result: result, - } - - <-result - close(result) -} - -func (throttle *throttleManager) run() { - for { - throttledUntil, ok := throttle.waitForThrottle() - if !ok { - break - } - - if !throttle.waitForReady(throttledUntil) { - break - } - } - - close(throttle.msgs) -} - -func (throttle *throttleManager) waitForThrottle() (time.Time, bool) { - for { - msg := <-throttle.msgs - if msg.query { - msg.result <- false - } else if msg.wait { - msg.result <- true - } else if msg.stop { - return time.Time{}, false - } else if msg.throttle { - return msg.timestamp, true - } - } -} - -func (throttle *throttleManager) waitForReady(throttledUntil time.Time) bool { - duration := throttledUntil.Sub(currentClock.Now()) - if duration <= 0 { - return true - } - - var notify []chan bool - - // --- Throttled and waiting --- - t := currentClock.NewTimer(duration) - - for { - select { - case <-t.C(): - for _, n := range notify { - n <- true - } - - return true - case msg := <-throttle.msgs: - if msg.query { - msg.result <- true - } else if msg.wait { - notify = append(notify, msg.result) - } else if msg.stop { - for _, n := range notify { - n <- false - } - - msg.result <- true - - return false - } else if msg.throttle { - if msg.timestamp.After(throttledUntil) { - throttledUntil = msg.timestamp - - if !t.Stop() { - <-t.C() - } - - t.Reset(throttledUntil.Sub(currentClock.Now())) - } - } - } - } -} diff --git a/application-insights/appinsights/transmitter.go b/application-insights/appinsights/transmitter.go deleted file mode 100644 index 33a8be03bb..0000000000 --- a/application-insights/appinsights/transmitter.go +++ /dev/null @@ -1,240 +0,0 @@ -package appinsights - -import ( - "bytes" - "compress/gzip" - "encoding/json" - "io/ioutil" - "net/http" - "sort" - "time" -) - -type transmitter interface { - Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) -} - -type httpTransmitter struct { - endpoint string - client *http.Client -} - -type transmissionResult struct { - statusCode int - retryAfter *time.Time - response *backendResponse -} - -// Structures returned by data collector -type backendResponse struct { - ItemsReceived int `json:"itemsReceived"` - ItemsAccepted int `json:"itemsAccepted"` - Errors itemTransmissionResults `json:"errors"` -} - -// This needs to be its own type because it implements sort.Interface -type itemTransmissionResults []*itemTransmissionResult - -type itemTransmissionResult struct { - Index int `json:"index"` - StatusCode int `json:"statusCode"` - Message string `json:"message"` -} - -const ( - successResponse = 200 - partialSuccessResponse = 206 - requestTimeoutResponse = 408 - tooManyRequestsResponse = 429 - tooManyRequestsOverExtendedTimeResponse = 439 - errorResponse = 500 - serviceUnavailableResponse = 503 -) - -func newTransmitter(endpointAddress string, client *http.Client) transmitter { - if client == nil { - client = http.DefaultClient - } - return &httpTransmitter{endpointAddress, client} -} - -func (transmitter *httpTransmitter) Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) { - diagnosticsWriter.Printf("--------- Transmitting %d items ---------", len(items)) - startTime := time.Now() - - // Compress the payload - var postBody bytes.Buffer - gzipWriter := gzip.NewWriter(&postBody) - if _, err := gzipWriter.Write(payload); err != nil { - diagnosticsWriter.Printf("Failed to compress the payload: %s", err.Error()) - gzipWriter.Close() - return nil, err - } - - gzipWriter.Close() - - req, err := http.NewRequest("POST", transmitter.endpoint, &postBody) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Encoding", "gzip") - req.Header.Set("Content-Type", "application/x-json-stream") - req.Header.Set("Accept-Encoding", "gzip, deflate") - - resp, err := transmitter.client.Do(req) - if err != nil { - diagnosticsWriter.Printf("Failed to transmit telemetry: %s", err.Error()) - return nil, err - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - diagnosticsWriter.Printf("Failed to read response from server: %s", err.Error()) - return nil, err - } - - duration := time.Since(startTime) - - result := &transmissionResult{statusCode: resp.StatusCode} - - // Grab Retry-After header - if retryAfterValue, ok := resp.Header[http.CanonicalHeaderKey("Retry-After")]; ok && len(retryAfterValue) == 1 { - if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterValue[0]); err == nil { - result.retryAfter = &retryAfterTime - } - } - - // Parse body, if possible - response := &backendResponse{} - if err := json.Unmarshal(body, &response); err == nil { - result.response = response - } - - // Write diagnostics - if diagnosticsWriter.hasListeners() { - diagnosticsWriter.Printf("Telemetry transmitted in %s", duration) - diagnosticsWriter.Printf("Response: %d", result.statusCode) - if result.response != nil { - diagnosticsWriter.Printf("Items accepted/received: %d/%d", result.response.ItemsAccepted, result.response.ItemsReceived) - if len(result.response.Errors) > 0 { - diagnosticsWriter.Printf("Errors:") - for _, err := range result.response.Errors { - if err.Index < len(items) { - diagnosticsWriter.Printf("#%d - %d %s", err.Index, err.StatusCode, err.Message) - diagnosticsWriter.Printf("Telemetry item:\n\t%s", string(items[err.Index:err.Index+1].serialize())) - } - } - } - } - } - - return result, nil -} - -func (result *transmissionResult) IsSuccess() bool { - return result.statusCode == successResponse || - // Partial response but all items accepted - (result.statusCode == partialSuccessResponse && - result.response != nil && - result.response.ItemsReceived == result.response.ItemsAccepted) -} - -func (result *transmissionResult) IsFailure() bool { - return result.statusCode != successResponse && result.statusCode != partialSuccessResponse -} - -func (result *transmissionResult) CanRetry() bool { - if result.IsSuccess() { - return false - } - - return result.statusCode == partialSuccessResponse || - result.retryAfter != nil || - (result.statusCode == requestTimeoutResponse || - result.statusCode == serviceUnavailableResponse || - result.statusCode == errorResponse || - result.statusCode == tooManyRequestsResponse || - result.statusCode == tooManyRequestsOverExtendedTimeResponse) -} - -func (result *transmissionResult) IsPartialSuccess() bool { - return result.statusCode == partialSuccessResponse && - result.response != nil && - result.response.ItemsReceived != result.response.ItemsAccepted -} - -func (result *transmissionResult) IsThrottled() bool { - return result.statusCode == tooManyRequestsResponse || - result.statusCode == tooManyRequestsOverExtendedTimeResponse || - result.retryAfter != nil -} - -func (result *itemTransmissionResult) CanRetry() bool { - return result.StatusCode == requestTimeoutResponse || - result.StatusCode == serviceUnavailableResponse || - result.StatusCode == errorResponse || - result.StatusCode == tooManyRequestsResponse || - result.StatusCode == tooManyRequestsOverExtendedTimeResponse -} - -func (result *transmissionResult) GetRetryItems(payload []byte, items telemetryBufferItems) ([]byte, telemetryBufferItems) { - if result.statusCode == partialSuccessResponse && result.response != nil { - // Make sure errors are ordered by index - sort.Sort(result.response.Errors) - - var resultPayload bytes.Buffer - resultItems := make(telemetryBufferItems, 0) - ptr := 0 - idx := 0 - - // Find each retryable error - for _, responseResult := range result.response.Errors { - if responseResult.CanRetry() { - // Advance ptr to start of desired line - for ; idx < responseResult.Index && ptr < len(payload); ptr++ { - if payload[ptr] == '\n' { - idx++ - } - } - - startPtr := ptr - - // Read to end of line - for ; idx == responseResult.Index && ptr < len(payload); ptr++ { - if payload[ptr] == '\n' { - idx++ - } - } - - // Copy item into output buffer - resultPayload.Write(payload[startPtr:ptr]) - resultItems = append(resultItems, items[responseResult.Index]) - } - } - - return resultPayload.Bytes(), resultItems - } else if result.CanRetry() { - return payload, items - } else { - return payload[:0], items[:0] - } -} - -// sort.Interface implementation for Errors[] list - -func (results itemTransmissionResults) Len() int { - return len(results) -} - -func (results itemTransmissionResults) Less(i, j int) bool { - return results[i].Index < results[j].Index -} - -func (results itemTransmissionResults) Swap(i, j int) { - tmp := results[i] - results[i] = results[j] - results[j] = tmp -} diff --git a/application-insights/appinsights/transmitter_test.go b/application-insights/appinsights/transmitter_test.go deleted file mode 100644 index 7fe3df5042..0000000000 --- a/application-insights/appinsights/transmitter_test.go +++ /dev/null @@ -1,508 +0,0 @@ -package appinsights - -import ( - "bytes" - "compress/gzip" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" -) - -type testServer struct { - server *httptest.Server - notify chan *testRequest - - responseData []byte - responseCode int - responseHeaders map[string]string -} - -type testRequest struct { - request *http.Request - body []byte -} - -func (server *testServer) Close() { - server.server.Close() - close(server.notify) -} - -func (server *testServer) ServeHTTP(writer http.ResponseWriter, req *http.Request) { - body, _ := ioutil.ReadAll(req.Body) - - hdr := writer.Header() - for k, v := range server.responseHeaders { - hdr[k] = []string{v} - } - - writer.WriteHeader(server.responseCode) - writer.Write(server.responseData) - - server.notify <- &testRequest{ - request: req, - body: body, - } -} - -func (server *testServer) waitForRequest(t *testing.T) *testRequest { - select { - case req := <-server.notify: - return req - case <-time.After(time.Second): - t.Fatal("Server did not receive request within a second") - return nil /* not reached */ - } -} - -type nullTransmitter struct{} - -func (transmitter *nullTransmitter) Transmit(payload []byte, items telemetryBufferItems) (*transmissionResult, error) { - return &transmissionResult{statusCode: successResponse}, nil -} - -func newTestClientServer() (transmitter, *testServer) { - server := &testServer{} - server.server = httptest.NewServer(server) - server.notify = make(chan *testRequest, 1) - server.responseCode = 200 - server.responseData = make([]byte, 0) - server.responseHeaders = make(map[string]string) - - client := newTransmitter(fmt.Sprintf("http://%s/v2/track", server.server.Listener.Addr().String()), nil) - - return client, server -} - -func newTestTlsClientServer(t *testing.T) (transmitter, *testServer) { - server := &testServer{} - server.server = httptest.NewTLSServer(server) - server.notify = make(chan *testRequest, 1) - server.responseCode = 200 - server.responseData = make([]byte, 0) - server.responseHeaders = make(map[string]string) - - client := newTransmitter(fmt.Sprintf("https://%s/v2/track", server.server.Listener.Addr().String()), server.server.Client()) - - return client, server -} - -func TestBasicTransitTls(t *testing.T) { - client, server := newTestTlsClientServer(t) - - doBasicTransmit(client, server, t) -} - -func TestBasicTransmit(t *testing.T) { - client, server := newTestClientServer() - - doBasicTransmit(client, server, t) -} - -func doBasicTransmit(client transmitter, server *testServer, t *testing.T) { - defer server.Close() - - server.responseData = []byte(`{"itemsReceived":3, "itemsAccepted":5, "errors":[]}`) - server.responseHeaders["Content-type"] = "application/json" - result, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) - if err != nil { - fmt.Println(err.Error()) - } - req := server.waitForRequest(t) - - if err != nil { - t.Errorf("err: %s", err.Error()) - } - - if req.request.Method != "POST" { - t.Error("request.Method") - } - - cencoding := req.request.Header[http.CanonicalHeaderKey("Content-Encoding")] - if len(cencoding) != 1 || cencoding[0] != "gzip" { - t.Errorf("Content-encoding: %q", cencoding) - } - - // Check for gzip magic number - if len(req.body) < 2 || req.body[0] != 0x1f || req.body[1] != 0x8b { - t.Fatal("Missing gzip magic number") - } - - // Decompress payload - reader, err := gzip.NewReader(bytes.NewReader(req.body)) - if err != nil { - t.Fatalf("Couldn't create gzip reader: %s", err.Error()) - } - - body, err := ioutil.ReadAll(reader) - reader.Close() - if err != nil { - t.Fatalf("Couldn't read compressed data: %s", err.Error()) - } - - if string(body) != "foobar" { - t.Error("body") - } - - ctype := req.request.Header[http.CanonicalHeaderKey("Content-Type")] - if len(ctype) != 1 || ctype[0] != "application/x-json-stream" { - t.Errorf("Content-type: %q", ctype) - } - - if result.statusCode != 200 { - t.Error("statusCode") - } - - if result.retryAfter != nil { - t.Error("retryAfter") - } - - if result.response == nil { - t.Fatal("response") - } - - if result.response.ItemsReceived != 3 { - t.Error("ItemsReceived") - } - - if result.response.ItemsAccepted != 5 { - t.Error("ItemsAccepted") - } - - if len(result.response.Errors) != 0 { - t.Error("response.Errors") - } -} - -func TestFailedTransmit(t *testing.T) { - client, server := newTestClientServer() - defer server.Close() - - server.responseCode = errorResponse - server.responseData = []byte(`{"itemsReceived":3, "itemsAccepted":0, "errors":[{"index": 2, "statusCode": 500, "message": "Hello"}]}`) - server.responseHeaders["Content-type"] = "application/json" - result, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) - server.waitForRequest(t) - - if err != nil { - t.Errorf("err: %s", err.Error()) - } - - if result.statusCode != errorResponse { - t.Error("statusCode") - } - - if result.retryAfter != nil { - t.Error("retryAfter") - } - - if result.response == nil { - t.Fatal("response") - } - - if result.response.ItemsReceived != 3 { - t.Error("ItemsReceived") - } - - if result.response.ItemsAccepted != 0 { - t.Error("ItemsAccepted") - } - - if len(result.response.Errors) != 1 { - t.Fatal("len(Errors)") - } - - if result.response.Errors[0].Index != 2 { - t.Error("Errors[0].index") - } - - if result.response.Errors[0].StatusCode != errorResponse { - t.Error("Errors[0].statusCode") - } - - if result.response.Errors[0].Message != "Hello" { - t.Error("Errors[0].message") - } -} - -func TestThrottledTransmit(t *testing.T) { - client, server := newTestClientServer() - defer server.Close() - - server.responseCode = errorResponse - server.responseData = make([]byte, 0) - server.responseHeaders["Content-type"] = "application/json" - server.responseHeaders["retry-after"] = "Wed, 09 Aug 2017 23:43:57 UTC" - result, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) - server.waitForRequest(t) - - if err != nil { - t.Errorf("err: %s", err.Error()) - } - - if result.statusCode != errorResponse { - t.Error("statusCode") - } - - if result.response != nil { - t.Fatal("response") - } - - if result.retryAfter == nil { - t.Fatal("retryAfter") - } - - if (*result.retryAfter).Unix() != 1502322237 { - t.Error("retryAfter.Unix") - } -} - -func TestTransmitDiagnostics(t *testing.T) { - client, server := newTestClientServer() - defer server.Close() - - var msgs []string - notify := make(chan bool, 1) - - NewDiagnosticsMessageListener(func(message string) error { - if message == "PING" { - notify <- true - } else { - msgs = append(msgs, message) - } - - return nil - }) - - defer resetDiagnosticsListeners() - - server.responseCode = errorResponse - server.responseData = []byte(`{"itemsReceived":1, "itemsAccepted":0, "errors":[{"index": 0, "statusCode": 500, "message": "Hello"}]}`) - server.responseHeaders["Content-type"] = "application/json" - _, err := client.Transmit([]byte("foobar"), make(telemetryBufferItems, 0)) - server.waitForRequest(t) - - // Wait for diagnostics to catch up. - diagnosticsWriter.Write("PING") - <-notify - - if err != nil { - t.Errorf("err: %s", err.Error()) - } - - // The last line should say "Errors:" and not include the error because the telemetry item wasn't submitted. - if !strings.Contains(msgs[len(msgs)-1], "Errors:") { - t.Errorf("Last line should say 'Errors:', with no errors listed. Instead: %s", msgs[len(msgs)-1]) - } - - // Go again but include telemetry items this time. - server.responseCode = errorResponse - server.responseData = []byte(`{"itemsReceived":1, "itemsAccepted":0, "errors":[{"index": 0, "statusCode": 500, "message": "Hello"}]}`) - server.responseHeaders["Content-type"] = "application/json" - _, err = client.Transmit([]byte("foobar"), telemetryBuffer(NewTraceTelemetry("World", Warning))) - server.waitForRequest(t) - - // Wait for diagnostics to catch up. - diagnosticsWriter.Write("PING") - <-notify - - if err != nil { - t.Errorf("err: %s", err.Error()) - } - - if !strings.Contains(msgs[len(msgs)-2], "500 Hello") { - t.Error("Telemetry error should be prefaced with result code and message") - } - - if !strings.Contains(msgs[len(msgs)-1], "World") { - t.Error("Raw telemetry item should be found on last line") - } - - close(notify) -} - -type resultProperties struct { - isSuccess bool - isFailure bool - canRetry bool - isThrottled bool - isPartialSuccess bool - retryableErrors bool -} - -func checkTransmitResult(t *testing.T, result *transmissionResult, expected *resultProperties) { - retryAfter := "" - if result.retryAfter != nil { - retryAfter = (*result.retryAfter).String() - } - response := "" - if result.response != nil { - response = fmt.Sprintf("%v", *result.response) - } - id := fmt.Sprintf("%d, retryAfter:%s, response:%s", result.statusCode, retryAfter, response) - - if result.IsSuccess() != expected.isSuccess { - t.Errorf("Expected IsSuccess() == %t [%s]", expected.isSuccess, id) - } - - if result.IsFailure() != expected.isFailure { - t.Errorf("Expected IsFailure() == %t [%s]", expected.isFailure, id) - } - - if result.CanRetry() != expected.canRetry { - t.Errorf("Expected CanRetry() == %t [%s]", expected.canRetry, id) - } - - if result.IsThrottled() != expected.isThrottled { - t.Errorf("Expected IsThrottled() == %t [%s]", expected.isThrottled, id) - } - - if result.IsPartialSuccess() != expected.isPartialSuccess { - t.Errorf("Expected IsPartialSuccess() == %t [%s]", expected.isPartialSuccess, id) - } - - // retryableErrors is true if CanRetry() and any error is recoverable - retryableErrors := false - if result.CanRetry() && result.response != nil { - for _, err := range result.response.Errors { - if err.CanRetry() { - retryableErrors = true - } - } - } - - if retryableErrors != expected.retryableErrors { - t.Errorf("Expected any(Errors.CanRetry) == %t [%s]", expected.retryableErrors, id) - } -} - -func TestTransmitResults(t *testing.T) { - retryAfter := time.Unix(1502322237, 0) - partialNoRetries := &backendResponse{ - ItemsAccepted: 3, - ItemsReceived: 5, - Errors: []*itemTransmissionResult{ - &itemTransmissionResult{Index: 2, StatusCode: 400, Message: "Bad 1"}, - &itemTransmissionResult{Index: 4, StatusCode: 400, Message: "Bad 2"}, - }, - } - - partialSomeRetries := &backendResponse{ - ItemsAccepted: 2, - ItemsReceived: 4, - Errors: []*itemTransmissionResult{ - &itemTransmissionResult{Index: 2, StatusCode: 400, Message: "Bad 1"}, - &itemTransmissionResult{Index: 4, StatusCode: 408, Message: "OK Later"}, - }, - } - - noneAccepted := &backendResponse{ - ItemsAccepted: 0, - ItemsReceived: 5, - Errors: []*itemTransmissionResult{ - &itemTransmissionResult{Index: 0, StatusCode: 500, Message: "Bad 1"}, - &itemTransmissionResult{Index: 1, StatusCode: 500, Message: "Bad 2"}, - &itemTransmissionResult{Index: 2, StatusCode: 500, Message: "Bad 3"}, - &itemTransmissionResult{Index: 3, StatusCode: 500, Message: "Bad 4"}, - &itemTransmissionResult{Index: 4, StatusCode: 500, Message: "Bad 5"}, - }, - } - - allAccepted := &backendResponse{ - ItemsAccepted: 6, - ItemsReceived: 6, - Errors: make([]*itemTransmissionResult, 0), - } - - checkTransmitResult(t, &transmissionResult{200, nil, allAccepted}, - &resultProperties{isSuccess: true}) - checkTransmitResult(t, &transmissionResult{206, nil, partialSomeRetries}, - &resultProperties{isPartialSuccess: true, canRetry: true, retryableErrors: true}) - checkTransmitResult(t, &transmissionResult{206, nil, partialNoRetries}, - &resultProperties{isPartialSuccess: true, canRetry: true}) - checkTransmitResult(t, &transmissionResult{206, nil, noneAccepted}, - &resultProperties{isPartialSuccess: true, canRetry: true, retryableErrors: true}) - checkTransmitResult(t, &transmissionResult{206, nil, allAccepted}, - &resultProperties{isSuccess: true}) - checkTransmitResult(t, &transmissionResult{400, nil, nil}, - &resultProperties{isFailure: true}) - checkTransmitResult(t, &transmissionResult{408, nil, nil}, - &resultProperties{isFailure: true, canRetry: true}) - checkTransmitResult(t, &transmissionResult{408, &retryAfter, nil}, - &resultProperties{isFailure: true, canRetry: true, isThrottled: true}) - checkTransmitResult(t, &transmissionResult{429, nil, nil}, - &resultProperties{isFailure: true, canRetry: true, isThrottled: true}) - checkTransmitResult(t, &transmissionResult{429, &retryAfter, nil}, - &resultProperties{isFailure: true, canRetry: true, isThrottled: true}) - checkTransmitResult(t, &transmissionResult{500, nil, nil}, - &resultProperties{isFailure: true, canRetry: true}) - checkTransmitResult(t, &transmissionResult{503, nil, nil}, - &resultProperties{isFailure: true, canRetry: true}) - checkTransmitResult(t, &transmissionResult{401, nil, nil}, - &resultProperties{isFailure: true}) - checkTransmitResult(t, &transmissionResult{408, nil, partialSomeRetries}, - &resultProperties{isFailure: true, canRetry: true, retryableErrors: true}) - checkTransmitResult(t, &transmissionResult{500, nil, partialSomeRetries}, - &resultProperties{isFailure: true, canRetry: true, retryableErrors: true}) -} - -func TestGetRetryItems(t *testing.T) { - mockClock() - defer resetClock() - - // Keep a pristine copy. - originalPayload, originalItems := makePayload() - - res1 := &transmissionResult{ - statusCode: 200, - response: &backendResponse{ItemsReceived: 7, ItemsAccepted: 7}, - } - - payload1, items1 := res1.GetRetryItems(makePayload()) - if len(payload1) > 0 || len(items1) > 0 { - t.Error("GetRetryItems shouldn't return anything") - } - - res2 := &transmissionResult{statusCode: 408} - - payload2, items2 := res2.GetRetryItems(makePayload()) - if string(originalPayload) != string(payload2) || len(items2) != 7 { - t.Error("GetRetryItems shouldn't return anything") - } - - res3 := &transmissionResult{ - statusCode: 206, - response: &backendResponse{ - ItemsReceived: 7, - ItemsAccepted: 4, - Errors: []*itemTransmissionResult{ - &itemTransmissionResult{Index: 1, StatusCode: 200, Message: "OK"}, - &itemTransmissionResult{Index: 3, StatusCode: 400, Message: "Bad"}, - &itemTransmissionResult{Index: 5, StatusCode: 408, Message: "Later"}, - &itemTransmissionResult{Index: 6, StatusCode: 500, Message: "Oops"}, - }, - }, - } - - payload3, items3 := res3.GetRetryItems(makePayload()) - expected3 := telemetryBufferItems{originalItems[5], originalItems[6]} - if string(payload3) != string(expected3.serialize()) || len(items3) != 2 { - t.Error("Unexpected result") - } -} - -func makePayload() ([]byte, telemetryBufferItems) { - buffer := telemetryBuffer() - for i := 0; i < 7; i++ { - tr := NewTraceTelemetry(fmt.Sprintf("msg%d", i+1), contracts.SeverityLevel(i%5)) - tr.Tags.Operation().SetId(fmt.Sprintf("op%d", i)) - buffer.add(tr) - } - - return buffer.serialize(), buffer -} diff --git a/application-insights/appinsights/uuid.go b/application-insights/appinsights/uuid.go deleted file mode 100644 index 08c6786f09..0000000000 --- a/application-insights/appinsights/uuid.go +++ /dev/null @@ -1,72 +0,0 @@ -package appinsights - -import ( - crand "crypto/rand" - "encoding/binary" - "io" - "math/rand" - "sync" - "time" - - "github.com/gofrs/uuid" -) - -// uuidGenerator is a wrapper for gofrs/uuid, an active fork of satori/go.uuid used for a few reasons: -// - Avoids build failures due to version differences when a project imports us but -// does not respect our vendoring. (satori/go.uuid#77, #71, #66, ...) -// - Avoids error output when creaing new UUID's: if the crypto reader fails, -// this will fallback on the standard library PRNG, since this is never used -// for a sensitive application. -// - Uses io.ReadFull to guarantee fully-populated UUID's (satori/go.uuid#73) -type uuidGenerator struct { - sync.Mutex - fallbackRand *rand.Rand - reader io.Reader -} - -var uuidgen *uuidGenerator = newUuidGenerator(crand.Reader) - -// newUuidGenerator creates a new uuiGenerator with the specified crypto random reader. -func newUuidGenerator(reader io.Reader) *uuidGenerator { - // Setup seed for fallback random generator - var seed int64 - b := make([]byte, 8) - if _, err := io.ReadFull(reader, b); err == nil { - seed = int64(binary.BigEndian.Uint64(b)) - } else { - // Otherwise just use the timestamp - seed = time.Now().UTC().UnixNano() - } - - return &uuidGenerator{ - reader: reader, - fallbackRand: rand.New(rand.NewSource(seed)), - } -} - -// newUUID generates a new V4 UUID -func (gen *uuidGenerator) newUUID() uuid.UUID { - //call the standard generator - u, err := uuid.NewV4() - //err will be either EOF or unexpected EOF - if err != nil { - gen.fallback(&u) - } - - return u -} - -// fallback populates the specified UUID with the standard library's PRNG -func (gen *uuidGenerator) fallback(u *uuid.UUID) { - gen.Lock() - defer gen.Unlock() - // This does not fail as per documentation - gen.fallbackRand.Read(u[:]) - u.SetVersion(uuid.V4) - u.SetVariant(uuid.VariantRFC4122) -} - -// newUUID generates a new V4 UUID -func newUUID() uuid.UUID { - return uuidgen.newUUID() -} diff --git a/application-insights/appinsights/uuid_test.go b/application-insights/appinsights/uuid_test.go deleted file mode 100644 index ebd6d1dee1..0000000000 --- a/application-insights/appinsights/uuid_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package appinsights - -import ( - "io" - "sync" - "testing" -) - -func TestNewUUID(t *testing.T) { - var start sync.WaitGroup - var finish sync.WaitGroup - - start.Add(1) - - goroutines := 250 - uuidsPerRoutine := 10 - results := make(chan string, 100) - - // Start normal set of UUID generation: - for i := 0; i < goroutines; i++ { - finish.Add(1) - go func() { - defer finish.Done() - start.Wait() - for t := 0; t < uuidsPerRoutine; t++ { - results <- newUUID().String() - } - }() - } - - // Start broken set of UUID generation - brokenGen := newUuidGenerator(&brokenReader{}) - for i := 0; i < goroutines; i++ { - finish.Add(1) - go func() { - defer finish.Done() - start.Wait() - for t := 0; t < uuidsPerRoutine; t++ { - results <- brokenGen.newUUID().String() - } - }() - } - - // Close the channel when all the goroutines have exited - go func() { - finish.Wait() - close(results) - }() - - used := make(map[string]bool) - start.Done() - for id := range results { - if _, ok := used[id]; ok { - t.Errorf("UUID was generated twice: %s", id) - } - - used[id] = true - } -} - -type brokenReader struct{} - -func (reader *brokenReader) Read(b []byte) (int, error) { - return 0, io.EOF -} diff --git a/application-insights/generateschema.ps1 b/application-insights/generateschema.ps1 deleted file mode 100644 index 0300a16387..0000000000 --- a/application-insights/generateschema.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -<# - -.SYNOPSIS - -Generate data contracts for Application Insights Go SDK from bond schema. - -.DESCRIPTION - -This is a convenience tool for generating the AI data contracts from the latest -bond schema. It requires BondSchemaGenerator.exe in order to operate. It also -requires the latest bond schema, but will check it out from github if it is not -present in the current directory. - -.PARAMETER BondSchemaGenerator - -The full path to BondSchemaGenerator.exe - -.PARAMETER SchemasDir - -The path to the directory that contains all of the input .bond files. - -.LINK https://github.com/microsoft/ApplicationInsights-Home - -#> - -[cmdletbinding()] -Param( - [Parameter(Mandatory=$true)] - [string] $BondSchemaGenerator, - [string] $SchemasDir -) - -function RunBondSchemaGenerator -{ - [cmdletbinding()] - Param( - [string] $Language, - [string[]] $Files, - [string] $Layout, - [string[]] $Omissions - ) - - $args = @("-v") - $args += @("-o", ".") - $args += @("-e", $Language) - $args += @("-t", $Layout) - - foreach ($file in $Files) { - $args += @("-i", $file) - } - - foreach ($omission in $Omissions) { - $args += @("--omit", $omission) - } - - & "$BondSchemaGenerator" $args 2>&1 -} - -$origpath = Get-Location - -try { - $scriptpath = $MyInvocation.MyCommand.Path - $dir = Split-Path $scriptpath - cd $dir - - if (-not (Test-Path $BondSchemaGenerator -PathType Leaf)) { - Write-Host "Could not find BondSchemaGenerator at $BondSchemaGenerator" - Write-Host "Please specify the full path" - Exit 1 - } - - if (-not $schemasDir) { - $schemasDir = ".\ApplicationInsights-Home\EndpointSpecs\Schemas\Bond" - - # Check locally. - if (-not (Test-Path .\ApplicationInsights-Home -PathType Container)) { - # Clone into it! - git clone https://github.com/microsoft/ApplicationInsights-Home.git - } - } - - $files = Get-ChildItem $schemasDir | % { "$schemasDir\$_" } - $omissions = @("Microsoft.Telemetry.Domain", "Microsoft.Telemetry.Base", "AI.AjaxCallData", "AI.PageViewPerfData") - - RunBondSchemaGenerator -Files $files -Language GoBondTemplateLanguage -Layout GoTemplateLayout -Omissions $omissions - RunBondSchemaGenerator -Files $files -Language GoContextTagsLanguage -Layout GoTemplateLayout -Omissions $omissions - - cd appinsights\contracts - go fmt -} finally { - cd $origpath -} diff --git a/application-insights/go.mod b/application-insights/go.mod deleted file mode 100644 index 78cee283e3..0000000000 --- a/application-insights/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module github.com/microsoft/ApplicationInsights-Go - -go 1.24.4 - -require ( - code.cloudfoundry.org/clock v1.41.0 - github.com/gofrs/uuid v4.4.0+incompatible - github.com/stretchr/testify v1.10.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/application-insights/go.sum b/application-insights/go.sum deleted file mode 100644 index 4566dc987c..0000000000 --- a/application-insights/go.sum +++ /dev/null @@ -1,38 +0,0 @@ -code.cloudfoundry.org/clock v1.41.0 h1:YiYQSEqcxswK+YtQ+NRIE31E1VNXkwb53Bb3zRmsoOM= -code.cloudfoundry.org/clock v1.41.0/go.mod h1:ncX4UpMuVwZooK7Rw7P+fsE2brLasFbPlibOOrZq40w= -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.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= -github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tedsuo/ifrit v0.0.0-20230516164442-7862c310ad26 h1:mWCRvpoEMVlslxEvvptKgIUb35va9yj9Oq5wGw/er5I= -github.com/tedsuo/ifrit v0.0.0-20230516164442-7862c310ad26/go.mod h1:0uD3VMXkZ7Bw0ojGCwDzebBBzPBXtzEZeXai+56BLX4= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index 66e2991719..a73d181a3d 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/hashicorp/go-version v1.7.0 + github.com/microsoft/ApplicationInsights-Go v0.4.4 github.com/nxadm/tail v1.4.11 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.37.0 @@ -127,7 +128,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 github.com/cilium/cilium v1.15.16 github.com/jsternberg/zap-logfmt v1.3.0 - github.com/microsoft/ApplicationInsights-Go v0.4.4 golang.org/x/sync v0.15.0 gotest.tools/v3 v3.5.2 k8s.io/kubectl v0.28.5 @@ -223,6 +223,7 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/dig v1.17.1 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect k8s.io/apiserver v0.30.14 // indirect @@ -250,7 +251,6 @@ require ( ) replace ( - github.com/microsoft/ApplicationInsights-Go => ./application-insights github.com/onsi/ginkgo => github.com/onsi/ginkgo v1.12.0 github.com/onsi/gomega => github.com/onsi/gomega v1.10.0 ) diff --git a/go.sum b/go.sum index c09df00e3b..efb8414ff0 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,18 @@ cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= <<<<<<< HEAD +<<<<<<< HEAD cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= code.cloudfoundry.org/clock v1.0.0 h1:kFXWQM4bxYvdBw2X8BbBeXwQNgfoWv1vqAk2ZZyBN2o= code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= ======= +======= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) code.cloudfoundry.org/clock v1.41.0 h1:YiYQSEqcxswK+YtQ+NRIE31E1VNXkwb53Bb3zRmsoOM= code.cloudfoundry.org/clock v1.41.0/go.mod h1:ncX4UpMuVwZooK7Rw7P+fsE2brLasFbPlibOOrZq40w= >>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) @@ -220,8 +226,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= @@ -354,6 +360,9 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= @@ -362,8 +371,11 @@ github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81T github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +<<<<<<< HEAD ======= >>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) +======= +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -481,12 +493,17 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= ======= >>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= ======= >>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) +======= +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= +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -500,11 +517,17 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= <<<<<<< HEAD +<<<<<<< HEAD github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= ======= >>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) +======= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= @@ -557,6 +580,23 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +<<<<<<< HEAD +======= +<<<<<<< HEAD +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +======= +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0 h1:Z6SbqeRZAl2OczfkFOqLx1BeYBDYehNjEnqluD7581Y= go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0/go.mod h1:XiglO+8SPMqM3Mqh5/rtxR1VHc63o8tb38QrU6tm4mU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= @@ -580,12 +620,18 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstF go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= <<<<<<< HEAD +<<<<<<< HEAD go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= ======= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= >>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) +======= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +>>>>>>> master +>>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -596,8 +642,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 08d8a88c1c4d5baa114108301247dd6909530109 Mon Sep 17 00:00:00 2001 From: beegiik Date: Tue, 15 Jul 2025 16:41:24 +0100 Subject: [PATCH 4/9] feat: Polishing --- go.sum | 66 ---------------------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/go.sum b/go.sum index efb8414ff0..3d9308f7c9 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,11 @@ cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -<<<<<<< HEAD -<<<<<<< HEAD cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= -code.cloudfoundry.org/clock v1.0.0 h1:kFXWQM4bxYvdBw2X8BbBeXwQNgfoWv1vqAk2ZZyBN2o= -code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= -======= -======= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) code.cloudfoundry.org/clock v1.41.0 h1:YiYQSEqcxswK+YtQ+NRIE31E1VNXkwb53Bb3zRmsoOM= code.cloudfoundry.org/clock v1.41.0/go.mod h1:ncX4UpMuVwZooK7Rw7P+fsE2brLasFbPlibOOrZq40w= ->>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-container-networking/zapai v0.0.3 h1:73druF1cnne5Ign/ztiXP99Ss5D+UJ80EL2mzPgNRhk= @@ -359,10 +348,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= @@ -371,11 +356,6 @@ github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81T github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -<<<<<<< HEAD -======= ->>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) -======= ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -491,19 +471,8 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -======= ->>>>>>> 4ec6dd349 (feat: create new telemetry handle with connection strings) github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -======= ->>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) -======= -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= ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -516,18 +485,9 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -<<<<<<< HEAD -<<<<<<< HEAD -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= -======= ->>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) -======= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= @@ -580,23 +540,6 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -<<<<<<< HEAD -======= -<<<<<<< HEAD -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -======= ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0 h1:Z6SbqeRZAl2OczfkFOqLx1BeYBDYehNjEnqluD7581Y= go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0/go.mod h1:XiglO+8SPMqM3Mqh5/rtxR1VHc63o8tb38QrU6tm4mU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= @@ -619,19 +562,10 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -<<<<<<< HEAD -<<<<<<< HEAD go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -======= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= ->>>>>>> 88d57179f (feat: Add application insights source code and remove dependency) -======= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= ->>>>>>> master ->>>>>>> ebec4dd0d (feat: Create connection string helper function and update telemetry handle) go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= From f360777df3edfc6a5eb587fb47525fde5f90cb7e Mon Sep 17 00:00:00 2001 From: beegiik Date: Wed, 16 Jul 2025 11:45:08 +0100 Subject: [PATCH 5/9] feat: Address PR comments --- aitelemetry/connection_string_parser.go | 27 ++++++++++++-------- aitelemetry/connection_string_parser_test.go | 12 ++++----- aitelemetry/telemetrywrapper.go | 18 ++++++------- aitelemetry/telemetrywrapper_test.go | 7 +++-- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/aitelemetry/connection_string_parser.go b/aitelemetry/connection_string_parser.go index e8db7dc031..f61a5e98fb 100644 --- a/aitelemetry/connection_string_parser.go +++ b/aitelemetry/connection_string_parser.go @@ -1,44 +1,51 @@ package aitelemetry import ( - "fmt" "strings" + + "github.com/pkg/errors" ) type connectionVars struct { - InstrumentationKey string - IngestionUrl string + instrumentationKey string + ingestionUrl string +} + +func (c *connectionVars) String() string { + return "InstrumentationKey=" + c.instrumentationKey + ";IngestionEndpoint=" + c.ingestionUrl } func parseConnectionString(connectionString string) (*connectionVars, error) { connectionVars := &connectionVars{} if connectionString == "" { - return nil, fmt.Errorf("Connection string cannot be empty") + return nil, errors.New("connection string cannot be empty") } pairs := strings.Split(connectionString, ";") for _, pair := range pairs { kv := strings.SplitN(pair, "=", 2) if len(kv) != 2 { - return nil, fmt.Errorf("Invalid connection string format: %s", pair) + return nil, errors.Errorf("invalid connection string format: %s", pair) } key, value := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]) if key == "" { - return nil, fmt.Errorf("Key in connection string cannot be empty") + return nil, errors.Errorf("key in connection string cannot be empty") } switch strings.ToLower(key) { case "instrumentationkey": - connectionVars.InstrumentationKey = value + connectionVars.instrumentationKey = value case "ingestionendpoint": - connectionVars.IngestionUrl = value + "v2.1/track" + if value != "" { + connectionVars.ingestionUrl = value + "v2.1/track" + } } } - if connectionVars.InstrumentationKey == "" || connectionVars.IngestionUrl == "" { - return nil, fmt.Errorf("Missing required fields in connection string") + if connectionVars.instrumentationKey == "" || connectionVars.ingestionUrl == "" { + return nil, errors.Errorf("missing required fields in connection string: %s", connectionVars) } return connectionVars, nil diff --git a/aitelemetry/connection_string_parser_test.go b/aitelemetry/connection_string_parser_test.go index 3044bf41d1..46d0af9d87 100644 --- a/aitelemetry/connection_string_parser_test.go +++ b/aitelemetry/connection_string_parser_test.go @@ -19,8 +19,8 @@ func TestParseConnectionString(t *testing.T) { name: "Valid connection string and instrumentation key", connectionString: connectionString, want: &connectionVars{ - InstrumentationKey: "00000000-0000-0000-0000-000000000000", - IngestionUrl: "https://ingestion.endpoint.com/v2.1/track", + instrumentationKey: "00000000-0000-0000-0000-000000000000", + ingestionUrl: "https://ingestion.endpoint.com/v2.1/track", }, wantErr: false, }, @@ -32,13 +32,13 @@ func TestParseConnectionString(t *testing.T) { }, { name: "Valid instrumentation key with missing ingestion endpoint", - connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=;", + connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=", want: nil, wantErr: true, }, { name: "Missing instrumentation key with valid ingestion endpoint", - connectionString: "InstrumentationKey=;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;", + connectionString: "InstrumentationKey=;IngestionEndpoint=https://ingestion.endpoint.com/", want: nil, wantErr: true, }, @@ -58,8 +58,8 @@ func TestParseConnectionString(t *testing.T) { } else { require.NoError(t, err, "Expected no error but got one") require.NotNil(t, got, "Expected a non-nil result") - require.Equal(t, tt.want.InstrumentationKey, got.InstrumentationKey, "Instrumentation Key does not match") - require.Equal(t, tt.want.IngestionUrl, got.IngestionUrl, "Ingestion URL does not match") + require.Equal(t, tt.want.instrumentationKey, got.instrumentationKey, "Instrumentation Key does not match") + require.Equal(t, tt.want.ingestionUrl, got.ingestionUrl, "Ingestion URL does not match") } }) } diff --git a/aitelemetry/telemetrywrapper.go b/aitelemetry/telemetrywrapper.go index 7e1a0fc6ac..56d8ffdc94 100644 --- a/aitelemetry/telemetrywrapper.go +++ b/aitelemetry/telemetrywrapper.go @@ -13,6 +13,7 @@ import ( "github.com/Azure/azure-container-networking/store" "github.com/microsoft/ApplicationInsights-Go/appinsights" "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" + "github.com/pkg/errors" ) const ( @@ -214,28 +215,25 @@ func NewAITelemetry( return th, nil } -// NewAITelemetry creates telemetry handle with user specified appinsights connection string. -func NewAITelemetryWithConnectionString( - cString string, - aiConfig AIConfig, -) (TelemetryHandle, error) { +// NewWithConnectionString creates telemetry handle with user specified appinsights connection string. +func NewWithConnectionString(connectionString string, aiConfig AIConfig) (TelemetryHandle, error) { debugMode = aiConfig.DebugMode - if cString == "" { + if connectionString == "" { debugLog("Empty connection string") - return nil, fmt.Errorf("AI connection string is empty") + return nil, errors.New("AI connection string is empty") } setAIConfigDefaults(&aiConfig) - connectionVars, err := parseConnectionString(cString) + connectionVars, err := parseConnectionString(connectionString) if err != nil { debugLog("Error parsing connection string: %v", err) return nil, err } - telemetryConfig := appinsights.NewTelemetryConfiguration(connectionVars.InstrumentationKey) - telemetryConfig.EndpointUrl = connectionVars.IngestionUrl + telemetryConfig := appinsights.NewTelemetryConfiguration(connectionVars.instrumentationKey) + telemetryConfig.EndpointUrl = connectionVars.ingestionUrl telemetryConfig.MaxBatchSize = aiConfig.BatchSize telemetryConfig.MaxBatchInterval = time.Duration(aiConfig.BatchInterval) * time.Second diff --git a/aitelemetry/telemetrywrapper_test.go b/aitelemetry/telemetrywrapper_test.go index 21a41a79d6..26a34984b5 100644 --- a/aitelemetry/telemetrywrapper_test.go +++ b/aitelemetry/telemetrywrapper_test.go @@ -19,7 +19,6 @@ var ( hostAgentUrl = "localhost:3501" getCloudResponse = "AzurePublicCloud" httpURL = "http://" + hostAgentUrl - // connectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" ) func TestMain(m *testing.M) { @@ -93,7 +92,7 @@ func TestEmptyAIKey(t *testing.T) { t.Errorf("Error initializing AI telemetry:%v", err) } - _, err = NewAITelemetryWithConnectionString("", aiConfig) + _, err = NewWithConnectionString("", aiConfig) if err == nil { t.Errorf("Error initializing AI telemetry with connection string:%v", err) } @@ -118,7 +117,7 @@ func TestNewAITelemetry(t *testing.T) { t.Errorf("Error initializing AI telemetry: %v", err) } - th2, err := NewAITelemetryWithConnectionString(connectionString, aiConfig) + th2, err := NewWithConnectionString(connectionString, aiConfig) if th2 == nil { t.Errorf("Error initializing AI telemetry with connection string: %v", err) } @@ -185,7 +184,7 @@ func TestClosewithoutSend(t *testing.T) { t.Errorf("Error initializing AI telemetry:%v", err) } - thtest2, err := NewAITelemetryWithConnectionString(connectionString, aiConfig) + thtest2, err := NewWithConnectionString(connectionString, aiConfig) if thtest2 == nil { t.Errorf("Error initializing AI telemetry with connection string:%v", err) } From 18a435850b02c32aa52296afe167696658dc80c3 Mon Sep 17 00:00:00 2001 From: beegiik Date: Thu, 17 Jul 2025 11:03:41 +0100 Subject: [PATCH 6/9] feat: Update test telemetry handle initialization --- aitelemetry/telemetrywrapper_test.go | 72 ++++++++++++++-------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/aitelemetry/telemetrywrapper_test.go b/aitelemetry/telemetrywrapper_test.go index 26a34984b5..fa242e61cb 100644 --- a/aitelemetry/telemetrywrapper_test.go +++ b/aitelemetry/telemetrywrapper_test.go @@ -15,7 +15,9 @@ import ( ) var ( - th TelemetryHandle + th1 TelemetryHandle + th2 TelemetryHandle + aiConfig AIConfig hostAgentUrl = "localhost:3501" getCloudResponse = "AzurePublicCloud" httpURL = "http://" + hostAgentUrl @@ -54,6 +56,28 @@ func TestMain(m *testing.M) { return } + aiConfig = AIConfig{ + AppName: "testapp", + AppVersion: "v1.0.26", + BatchSize: 4096, + BatchInterval: 2, + RefreshTimeout: 10, + GetEnvRetryCount: 1, + GetEnvRetryWaitTimeInSecs: 2, + DebugMode: true, + DisableMetadataRefreshThread: true, + } + + th1, err = NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) + if th1 == nil { + fmt.Printf("Error initializing AI telemetry: %v", err) + } + + th2, err = NewWithConnectionString(connectionString, aiConfig) + if th2 == nil { + fmt.Printf("Error initializing AI telemetry with connection string: %v", err) + } + exitCode := m.Run() if runtime.GOOS == "linux" { @@ -78,15 +102,6 @@ func handleGetCloud(w http.ResponseWriter, req *http.Request) { func TestEmptyAIKey(t *testing.T) { var err error - aiConfig := AIConfig{ - AppName: "testapp", - AppVersion: "v1.0.26", - BatchSize: 4096, - BatchInterval: 2, - RefreshTimeout: 10, - DebugMode: true, - DisableMetadataRefreshThread: true, - } _, err = NewAITelemetry(httpURL, "", aiConfig) if err == nil { t.Errorf("Error initializing AI telemetry:%v", err) @@ -101,17 +116,6 @@ func TestEmptyAIKey(t *testing.T) { func TestNewAITelemetry(t *testing.T) { var err error - aiConfig := AIConfig{ - AppName: "testapp", - AppVersion: "v1.0.26", - BatchSize: 4096, - BatchInterval: 2, - RefreshTimeout: 10, - GetEnvRetryCount: 1, - GetEnvRetryWaitTimeInSecs: 2, - DebugMode: true, - DisableMetadataRefreshThread: true, - } th1, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) if th1 == nil { t.Errorf("Error initializing AI telemetry: %v", err) @@ -131,7 +135,8 @@ func TestTrackMetric(t *testing.T) { } metric.CustomDimensions["dim1"] = "col1" - th.TrackMetric(metric) + th1.TrackMetric(metric) + th2.TrackMetric(metric) } func TestTrackLog(t *testing.T) { @@ -142,7 +147,8 @@ func TestTrackLog(t *testing.T) { } report.CustomDimensions["dim1"] = "col1" - th.TrackLog(report) + th1.TrackLog(report) + th2.TrackLog(report) } func TestTrackEvent(t *testing.T) { @@ -154,31 +160,23 @@ func TestTrackEvent(t *testing.T) { event.Properties["P1"] = "V1" event.Properties["P2"] = "V2" - th.TrackEvent(event) + th1.TrackEvent(event) + th2.TrackEvent(event) } func TestFlush(t *testing.T) { - th.Flush() + th1.Flush() + th2.Flush() } func TestClose(t *testing.T) { - th.Close(10) + th1.Close(10) + th2.Close(10) } func TestClosewithoutSend(t *testing.T) { var err error - aiConfig := AIConfig{ - AppName: "testapp", - AppVersion: "v1.0.26", - BatchSize: 4096, - BatchInterval: 2, - DisableMetadataRefreshThread: true, - RefreshTimeout: 10, - GetEnvRetryCount: 1, - GetEnvRetryWaitTimeInSecs: 2, - } - thtest, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) if thtest == nil { t.Errorf("Error initializing AI telemetry:%v", err) From 86dfc35926395396b9a8194703312cbec5cf25a3 Mon Sep 17 00:00:00 2001 From: beegiik Date: Thu, 17 Jul 2025 12:19:05 +0100 Subject: [PATCH 7/9] feat: Re-initialise telemetry handles to fix race conditions during test execution --- aitelemetry/telemetrywrapper_test.go | 56 +++++++++++++--------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/aitelemetry/telemetrywrapper_test.go b/aitelemetry/telemetrywrapper_test.go index fa242e61cb..2f033e5e05 100644 --- a/aitelemetry/telemetrywrapper_test.go +++ b/aitelemetry/telemetrywrapper_test.go @@ -15,8 +15,6 @@ import ( ) var ( - th1 TelemetryHandle - th2 TelemetryHandle aiConfig AIConfig hostAgentUrl = "localhost:3501" getCloudResponse = "AzurePublicCloud" @@ -68,16 +66,6 @@ func TestMain(m *testing.M) { DisableMetadataRefreshThread: true, } - th1, err = NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) - if th1 == nil { - fmt.Printf("Error initializing AI telemetry: %v", err) - } - - th2, err = NewWithConnectionString(connectionString, aiConfig) - if th2 == nil { - fmt.Printf("Error initializing AI telemetry with connection string: %v", err) - } - exitCode := m.Run() if runtime.GOOS == "linux" { @@ -99,6 +87,20 @@ func handleGetCloud(w http.ResponseWriter, req *http.Request) { w.Write([]byte(getCloudResponse)) } +func initTelemetry(t *testing.T) (TelemetryHandle, TelemetryHandle) { + th1, err1 := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) + if err1 != nil { + fmt.Printf("Error initializing AI telemetry: %v", err1) + } + + th2, err2 := NewWithConnectionString(connectionString, aiConfig) + if err2 != nil { + fmt.Printf("Error initializing AI telemetry with connection string: %v", err2) + } + + return th1, th2 +} + func TestEmptyAIKey(t *testing.T) { var err error @@ -116,18 +118,19 @@ func TestEmptyAIKey(t *testing.T) { func TestNewAITelemetry(t *testing.T) { var err error - th1, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) + th1, th2 := initTelemetry(t) if th1 == nil { t.Errorf("Error initializing AI telemetry: %v", err) } - th2, err := NewWithConnectionString(connectionString, aiConfig) if th2 == nil { t.Errorf("Error initializing AI telemetry with connection string: %v", err) } } func TestTrackMetric(t *testing.T) { + th1, th2 := initTelemetry(t) + metric := Metric{ Name: "test", Value: 1.0, @@ -140,6 +143,8 @@ func TestTrackMetric(t *testing.T) { } func TestTrackLog(t *testing.T) { + th1, th2 := initTelemetry(t) + report := Report{ Message: "test", Context: "10a", @@ -152,6 +157,8 @@ func TestTrackLog(t *testing.T) { } func TestTrackEvent(t *testing.T) { + th1, th2 := initTelemetry(t) + event := Event{ EventName: "testEvent", ResourceID: "SomeResourceId", @@ -165,28 +172,15 @@ func TestTrackEvent(t *testing.T) { } func TestFlush(t *testing.T) { + th1, th2 := initTelemetry(t) + th1.Flush() th2.Flush() } func TestClose(t *testing.T) { + th1, th2 := initTelemetry(t) + th1.Close(10) th2.Close(10) } - -func TestClosewithoutSend(t *testing.T) { - var err error - - thtest, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) - if thtest == nil { - t.Errorf("Error initializing AI telemetry:%v", err) - } - - thtest2, err := NewWithConnectionString(connectionString, aiConfig) - if thtest2 == nil { - t.Errorf("Error initializing AI telemetry with connection string:%v", err) - } - - thtest.Close(10) - thtest2.Close(10) -} From 8ed839fe5db5cf2497574bf54f97015988a3b015 Mon Sep 17 00:00:00 2001 From: beegiik Date: Wed, 23 Jul 2025 18:03:50 +0100 Subject: [PATCH 8/9] feat: revert string formatting changes --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a73d181a3d..bd9bc4255b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Azure/azure-container-networking -go 1.24.4 +go 1.23.2 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 From 16ec957534ef7705b5592179cdb64d5d91343ae7 Mon Sep 17 00:00:00 2001 From: beegiik Date: Wed, 23 Jul 2025 18:55:09 +0100 Subject: [PATCH 9/9] feat: fix lint --- aitelemetry/connection_string_parser.go | 8 ++++---- aitelemetry/connection_string_parser_test.go | 10 +++++----- aitelemetry/telemetrywrapper.go | 4 ++-- aitelemetry/telemetrywrapper_test.go | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/aitelemetry/connection_string_parser.go b/aitelemetry/connection_string_parser.go index f61a5e98fb..b4e7223a22 100644 --- a/aitelemetry/connection_string_parser.go +++ b/aitelemetry/connection_string_parser.go @@ -8,11 +8,11 @@ import ( type connectionVars struct { instrumentationKey string - ingestionUrl string + ingestionURL string } func (c *connectionVars) String() string { - return "InstrumentationKey=" + c.instrumentationKey + ";IngestionEndpoint=" + c.ingestionUrl + return "InstrumentationKey=" + c.instrumentationKey + ";IngestionEndpoint=" + c.ingestionURL } func parseConnectionString(connectionString string) (*connectionVars, error) { @@ -39,12 +39,12 @@ func parseConnectionString(connectionString string) (*connectionVars, error) { connectionVars.instrumentationKey = value case "ingestionendpoint": if value != "" { - connectionVars.ingestionUrl = value + "v2.1/track" + connectionVars.ingestionURL = value + "v2.1/track" } } } - if connectionVars.instrumentationKey == "" || connectionVars.ingestionUrl == "" { + if connectionVars.instrumentationKey == "" || connectionVars.ingestionURL == "" { return nil, errors.Errorf("missing required fields in connection string: %s", connectionVars) } diff --git a/aitelemetry/connection_string_parser_test.go b/aitelemetry/connection_string_parser_test.go index 46d0af9d87..adac67cd06 100644 --- a/aitelemetry/connection_string_parser_test.go +++ b/aitelemetry/connection_string_parser_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" ) -const connectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=11111111-1111-1111-1111-111111111111" +const connectionString = "InstrumentationKey=0000-0000-0000-0000-0000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=1111-1111-1111-1111-1111" func TestParseConnectionString(t *testing.T) { tests := []struct { @@ -19,8 +19,8 @@ func TestParseConnectionString(t *testing.T) { name: "Valid connection string and instrumentation key", connectionString: connectionString, want: &connectionVars{ - instrumentationKey: "00000000-0000-0000-0000-000000000000", - ingestionUrl: "https://ingestion.endpoint.com/v2.1/track", + instrumentationKey: "0000-0000-0000-0000-0000", + ingestionURL: "https://ingestion.endpoint.com/v2.1/track", }, wantErr: false, }, @@ -32,7 +32,7 @@ func TestParseConnectionString(t *testing.T) { }, { name: "Valid instrumentation key with missing ingestion endpoint", - connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=", + connectionString: "InstrumentationKey=0000-0000-0000-0000-0000;IngestionEndpoint=", want: nil, wantErr: true, }, @@ -59,7 +59,7 @@ func TestParseConnectionString(t *testing.T) { require.NoError(t, err, "Expected no error but got one") require.NotNil(t, got, "Expected a non-nil result") require.Equal(t, tt.want.instrumentationKey, got.instrumentationKey, "Instrumentation Key does not match") - require.Equal(t, tt.want.ingestionUrl, got.ingestionUrl, "Ingestion URL does not match") + require.Equal(t, tt.want.ingestionURL, got.ingestionURL, "Ingestion URL does not match") } }) } diff --git a/aitelemetry/telemetrywrapper.go b/aitelemetry/telemetrywrapper.go index 56d8ffdc94..20dde0baa2 100644 --- a/aitelemetry/telemetrywrapper.go +++ b/aitelemetry/telemetrywrapper.go @@ -162,7 +162,7 @@ func isPublicEnvironment(url string, retryCount, waitTimeInSecs int) (bool, erro return true, nil } else if err == nil { debugLog("[AppInsights] This is not azure public cloud:%s", cloudName) - return false, fmt.Errorf("not an azure public cloud: %s", cloudName) + return false, errors.Errorf("not an azure public cloud: %s", cloudName) } debugLog("GetAzureCloud returned err :%v", err) @@ -233,7 +233,7 @@ func NewWithConnectionString(connectionString string, aiConfig AIConfig) (Teleme } telemetryConfig := appinsights.NewTelemetryConfiguration(connectionVars.instrumentationKey) - telemetryConfig.EndpointUrl = connectionVars.ingestionUrl + telemetryConfig.EndpointUrl = connectionVars.ingestionURL telemetryConfig.MaxBatchSize = aiConfig.BatchSize telemetryConfig.MaxBatchInterval = time.Duration(aiConfig.BatchInterval) * time.Second diff --git a/aitelemetry/telemetrywrapper_test.go b/aitelemetry/telemetrywrapper_test.go index 2f033e5e05..363f9b90a8 100644 --- a/aitelemetry/telemetrywrapper_test.go +++ b/aitelemetry/telemetrywrapper_test.go @@ -87,7 +87,7 @@ func handleGetCloud(w http.ResponseWriter, req *http.Request) { w.Write([]byte(getCloudResponse)) } -func initTelemetry(t *testing.T) (TelemetryHandle, TelemetryHandle) { +func initTelemetry(_ *testing.T) (th1, th2 TelemetryHandle) { th1, err1 := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig) if err1 != nil { fmt.Printf("Error initializing AI telemetry: %v", err1) @@ -98,7 +98,7 @@ func initTelemetry(t *testing.T) (TelemetryHandle, TelemetryHandle) { fmt.Printf("Error initializing AI telemetry with connection string: %v", err2) } - return th1, th2 + return } func TestEmptyAIKey(t *testing.T) {