├── .dockerignore ├── .gitignore ├── CODE_OF_CONDUCT.md ├── DotNetObservabilitySample.sln ├── LICENSE ├── NuGet.config ├── README.md ├── SECURITY.md ├── docker-compose.dcproj ├── docker-compose.override.yml ├── docker-compose.yml ├── media ├── ai-search-logs.png ├── ai-tracing.png ├── grafana-metrics.png ├── jaeger-tracing.png └── sample-app-overview.png ├── prometheus.yml ├── quickstart ├── prometheus-grafana │ ├── docker-compose.yml │ └── prometheus.yml └── sample │ └── docker-compose.yml └── src ├── Sample.Common ├── ApplicationInformation.cs ├── ApplicationInsightsLink.cs ├── CloudRoleTelemetryInitializer.cs ├── ConfigurationExtensions.cs ├── Constants.cs ├── EnqueuedMessage.cs ├── FailGenerator.cs ├── GeneratedFailureException.cs ├── IAppMetrics.cs ├── OpenTelemetryExtensions.cs ├── PromotheusExporterHostedService.cs ├── Sample.Common.csproj ├── SampleAppOptions.cs └── SampleServiceCollectionExtensions.cs ├── Sample.MainApi ├── Controllers │ └── MainController.cs ├── Dockerfile ├── HelloRequest.cs ├── HostedServices │ └── HelloHostedService.cs ├── IRabbitMQProducer.cs ├── Metrics.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── RabbitMQProducer.cs ├── Sample.MainApi.csproj ├── Startup.cs └── appsettings.json ├── Sample.RabbitMQCollector ├── ActivityEnabledModel.cs ├── ActivityExtensions.cs ├── ApplicationInsights │ ├── DiagnosticSourceListener.cs │ ├── RabbitMQApplicationInsightsModule.cs │ ├── RabbitMQCollector.cs │ └── RabbitMQSourceListener.cs ├── Constants.cs ├── IModelExtensions.cs ├── OpenTelemetry │ ├── RabbitMQCollector.cs │ └── RabbitMQListener.cs ├── Sample.RabbitMQCollector.csproj └── TraceParent.cs ├── Sample.RabbitMQProcessor ├── Dockerfile ├── InvalidEventNameException.cs ├── Metrics.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Sample.RabbitMQProcessor.csproj ├── WebQueueConsumerHostedService.cs └── appsettings.json └── Sample.TimeApi ├── Controllers └── TimeController.cs ├── Data ├── IDeviceRepository.cs ├── OpenTelemetryCollectingDeviceRepository.cs └── SqlDeviceRepository.cs ├── Dockerfile ├── Program.cs ├── Properties └── launchSettings.json ├── Sample.TimeApi.csproj ├── Startup.cs └── appsettings.json /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | .env -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /DotNetObservabilitySample.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.29613.14 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQProcessor", "src\Sample.RabbitMQProcessor\Sample.RabbitMQProcessor.csproj", "{B2900141-0E8C-46EE-908E-F98218960804}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Common", "src\Sample.Common\Sample.Common.csproj", "{39CCAC82-947E-4229-85C1-C34D3B569BE4}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.TimeApi", "src\Sample.TimeApi\Sample.TimeApi.csproj", "{883EB697-F742-422B-ADC9-3A89C00B5424}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.MainApi", "src\Sample.MainApi\Sample.MainApi.csproj", "{4EA49E72-7529-4CFD-8942-0086BC0E43F4}" 12 | EndProject 13 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{D9851614-526F-43C3-B9E2-F93B0F0E1448}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.RabbitMQCollector", "src\Sample.RabbitMQCollector\Sample.RabbitMQCollector.csproj", "{7A7BF83E-DA37-47FC-83CF-104D3EA9B36F}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {B2900141-0E8C-46EE-908E-F98218960804}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {B2900141-0E8C-46EE-908E-F98218960804}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {B2900141-0E8C-46EE-908E-F98218960804}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {B2900141-0E8C-46EE-908E-F98218960804}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {39CCAC82-947E-4229-85C1-C34D3B569BE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {39CCAC82-947E-4229-85C1-C34D3B569BE4}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {39CCAC82-947E-4229-85C1-C34D3B569BE4}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {39CCAC82-947E-4229-85C1-C34D3B569BE4}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {883EB697-F742-422B-ADC9-3A89C00B5424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {883EB697-F742-422B-ADC9-3A89C00B5424}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {883EB697-F742-422B-ADC9-3A89C00B5424}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {883EB697-F742-422B-ADC9-3A89C00B5424}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {4EA49E72-7529-4CFD-8942-0086BC0E43F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {4EA49E72-7529-4CFD-8942-0086BC0E43F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {4EA49E72-7529-4CFD-8942-0086BC0E43F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {4EA49E72-7529-4CFD-8942-0086BC0E43F4}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {D9851614-526F-43C3-B9E2-F93B0F0E1448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {D9851614-526F-43C3-B9E2-F93B0F0E1448}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {D9851614-526F-43C3-B9E2-F93B0F0E1448}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {D9851614-526F-43C3-B9E2-F93B0F0E1448}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {7A7BF83E-DA37-47FC-83CF-104D3EA9B36F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {7A7BF83E-DA37-47FC-83CF-104D3EA9B36F}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {7A7BF83E-DA37-47FC-83CF-104D3EA9B36F}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {7A7BF83E-DA37-47FC-83CF-104D3EA9B36F}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(ExtensibilityGlobals) = postSolution 52 | SolutionGuid = {5170C652-61A9-4542-954E-246518C6C40B} 53 | EndGlobalSection 54 | GlobalSection(MonoDevelopProperties) = preSolution 55 | version = 0.2 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | products: 6 | - dotnet 7 | description: "Adding observability to ASP.NET Core application using OpenTelemetry and Application Insights" 8 | urlFragment: "update-this-to-unique-url-stub" 9 | --- 10 | 11 | # Official Microsoft Sample 12 | 13 | 20 | 21 | This sample application takes a look at current options to implement observability in a ASP.NET Core. It uses OpenTelemetry and Application Insights SDKs to illustrate how logging, tracing and metrics to monitor an application. It contains a distributed transaction example trace including REST, dependencies and RabbitMQ processing. 22 | 23 | ![Sample application](./media/sample-app-overview.png) 24 | 25 | ## Contents 26 | 27 | | File/folder | Description | 28 | |-------------------|--------------------------------------------| 29 | | `src` | Sample source code. | 30 | | `quickstart` | Quick start using docker-compose and pre-built images. | 31 | | `.gitignore` | Define what to ignore at commit time. | 32 | | `CHANGELOG.md` | List of changes to the sample. | 33 | | `CONTRIBUTING.md` | Guidelines for contributing to the sample. | 34 | | `README.md` | This README file. | 35 | | `LICENSE` | The license for the sample. | 36 | 37 | ## Prerequisites 38 | 39 | Sample application can be executed in two ways: 40 | 41 | - Using docker-compose. It is a great way to get started. 42 | - Downloading source code and running it locally. Using Visual Studio or another IDE. In this case the .NET Core 3.1 SDK is required. To use Jaeger and Prometheues Docker is recommended. 43 | 44 | ## Setup - Quickstart with docker-compose 45 | 46 | To run the application using pre-built images and docker-compose following the guideline below: 47 | 48 | ### Using OpenTelemetry 49 | 50 | 1. Clone this repository 51 | 1. Open terminal under `quickstart/sample` 52 | 1. Execute `docker-compose up` (-d if you don't wish to see console logs) 53 | 1. View traces in [Jaeger](http://localhost:16686/) 54 | 1. View metrics by searching for "Enqueued_Item" in [Prometheus](http://localhost:9090) 55 | 1. Build dashboards in [Grafana](http://localhost:3000/) (admin/password1) 56 | 57 | ### Using Application Insights SDK 58 | 59 | 1. Clone this repository 60 | 1. Open terminal under `quickstart/sample` 61 | 1. Create file `quickstart/sample/.env` with following content: 62 | 63 | ```env 64 | USE_APPLICATIONINSIGHTS=true 65 | USE_OPENTELEMETRY=false 66 | AI_INSTRUMENTATIONKEY= 67 | ``` 68 | 69 | 4. Execute `docker-compose up` (-d if you don't wish to see console logs) 70 | 5. View logs, traces and metrics in Azure Portal Application Insights 71 | 72 | ## Setup - Compile/debug locally 73 | 74 | Clone or download the sample from this repository, then open the solution found in root folder using your favorite IDE. 75 | 76 | Before running ensure the following dependencies are available: 77 | 78 | - SQL Server is available at `server=localhost;user id=sa;password=Pass@Word1;`
79 | A way to accomplish it is to run as a linux docker container: 80 | 81 | ```bash 82 | docker run --name sqlserver -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@Word1" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 83 | ``` 84 | 85 | - When using OpenTelemetry, ensure Jaeger is running locally 86 | 87 | ```bash 88 | docker run -d --name jaeger \ 89 | -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ 90 | -p 5775:5775/udp \ 91 | -p 6831:6831/udp \ 92 | -p 6832:6832/udp \ 93 | -p 5778:5778 \ 94 | -p 16686:16686 \ 95 | -p 14268:14268 \ 96 | -p 9411:9411 \ 97 | jaegertracing/all-in-one 98 | ``` 99 | 100 | - When using OpenTelemetry, in order to visualize metrics ensure Grafana and Prometheus are running locally. A docker-compose file is ready to run under `quickstart/prometheus-grafana`. Open terminal in the mentioned folder and execute `docker-compose up -d`.
To visualize it, open Grafana on your browser at [http://localhost:3000](http://localhost:3000) (credentials are admin/password1). Next, add Prometheus as data source (URL is http://prometheus:9090). 101 | 102 | - When using Application Insights, ensure the instrumentation key is set (a simpler way to provide settings to all applications is to create file appsettings.Development.json in folder ./shared): 103 | 104 | ```json 105 | { 106 | "SampleApp": { 107 | "UseApplicationInsights": "true", 108 | "UseOpenTelemetry": "false", 109 | "ApplicationInsightsInstrumentationKey": "" 110 | } 111 | } 112 | ``` 113 | 114 | #### Generating load 115 | 116 | The application will only collect data once it starts to receive load. To generate load use the following scripts: 117 | 118 | Enqueuing from "WebSiteA" every 2 seconds 119 | 120 | ```cmd 121 | watch -n 2 curl --request GET http://localhost:5001/api/enqueue/WebSiteA 122 | ``` 123 | 124 | ```powershell 125 | while (1) {Invoke-WebRequest -Uri http://localhost:5001/api/enqueue/WebSiteA; sleep 2} 126 | ``` 127 | 128 | Enqueuing from "WebSiteB" every 10 seconds 129 | 130 | ```cmd 131 | watch -n 10 curl --request GET http://localhost:5001/api/enqueue/WebSiteB 132 | ``` 133 | 134 | ```powershell 135 | while (1) {Invoke-WebRequest -Uri http://localhost:5001/api/enqueue/WebSiteB; sleep 10} 136 | ``` 137 | 138 | Enqueuing from "WebSiteC" every 30 seconds 139 | 140 | ```cmd 141 | watch -n 30 curl --request GET http://localhost:5001/api/enqueue/WebSiteC 142 | ``` 143 | 144 | ```powershell 145 | while (1) {Invoke-WebRequest -Uri http://localhost:5001/api/enqueue/WebSiteC; sleep 30} 146 | ``` 147 | 148 | ## Key concepts 149 | 150 | Goal of the sample application is to demonstrate ways you can add the 3 observability pillars to your ASP.NET Core application: 151 | 152 | ### Logging 153 | 154 | Collects information about events happening in the system, helping the team analyze unexpected application behavior. Searching through the logs of suspect services can provide the necessary hint to identify the problem root cause: service is throwing out of memory exceptions, app configuration does not reflect expected values, calls to external service have incorrect address, calls to external service returns unexpected results, incoming requests have unexpected input, etc. 155 | 156 | Logging with Application Insights: 157 | 158 | ![Logging](./media/ai-search-logs.png) 159 | 160 | ### Traces 161 | 162 | Collects information in order to create an end-to-end view of how transactions are executed in a distributed system. A trace is like a stack trace spanning multiple applications. Once a problem has been recognized, traces are a good starting point in identifying the source in distributed operations: calls from service A to B are taking longer than normal, service payment calls are failing, etc. 163 | 164 | Traces with Jaeger: 165 | 166 | ![Jaeger Tracing](./media/jaeger-tracing.png) 167 | 168 | Traces with Application Insights: 169 | 170 | ![Application Insights Tracing](./media/ai-tracing.png) 171 | 172 | ### Metrics 173 | 174 | Provide a near real-time indication of how the system is running. Can be leveraged to build alerts, allowing proactive reactance to unexpected values. As opposed to logs and traces, the amount of data collected using metrics remains constant as the system load increases. Application problems are often first detected through abnormal metric values: CPU usage is higher than before, payment error count is spiking, queued item count keeps growing. 175 | 176 | Metrics with 177 | ![Grafane/Prometheus Metrics](./media/grafana-metrics.png) 178 | 179 | ## Contributing 180 | 181 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 182 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 183 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 184 | 185 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 186 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 187 | provided by the bot. You will only need to do this once across all repos using our CLA. 188 | 189 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 190 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 191 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 192 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | 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/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | 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 the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | 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://www.microsoft.com/msrc). 18 | 19 | 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: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | 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://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | d9851614-526f-43c3-b9e2-f93b0f0e1448 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort}/api/dbtime 9 | sample.mainapi 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | sample.mainapi: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | ports: 8 | - "5001:80" 9 | 10 | sample.timeapi: 11 | environment: 12 | - ASPNETCORE_ENVIRONMENT=Development 13 | ports: 14 | - "5002:80" 15 | 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | sample.mainapi: 5 | image: ${DOCKER_REGISTRY-}dotnetobservabilitysample-mainapi:${TAG-latest} 6 | environment: 7 | - SampleApp__RabbitMQHostName=rabbitmq 8 | - SampleApp__TimeAPIUrl=http://sample.timeapi 9 | - SampleApp__UseApplicationInsights=${USE_APPLICATIONINSIGHTS-false} 10 | - SampleApp__UseOpenTelemetry=${USE_OPENTELEMETRY-true} 11 | - SampleApp__ApplicationInsightsInstrumentationKey=${AI_INSTRUMENTATIONKEY-} 12 | - SampleApp__ApplicationInsightsForOpenTelemetryInstrumentationKey=${AI_INSTRUMENTATIONKEY_OPENTELEMETRY-} 13 | - OpenTelemetry__Prometheus__Url=http://sample.mainapi:9184/metrics/ 14 | - OpenTelemetry__Jaeger__AgentHost=${JAEGER_AGENTHOST-jaeger} 15 | depends_on: 16 | - rabbitmq 17 | - sample.timeapi 18 | - prometheus 19 | - jaeger 20 | build: 21 | context: . 22 | dockerfile: src/Sample.MainApi/Dockerfile 23 | ports: 24 | - "5001:80" 25 | - "9184:9184" 26 | 27 | sample.rabbitmqprocessor: 28 | image: ${DOCKER_REGISTRY-}dotnetobservabilitysample-rabbitmqprocessor:${TAG-latest} 29 | environment: 30 | - SampleApp__RabbitMQHostName=rabbitmq 31 | - SampleApp__TimeAPIUrl=http://sample.timeapi 32 | - SampleApp__UseApplicationInsights=${USE_APPLICATIONINSIGHTS-false} 33 | - SampleApp__UseOpenTelemetry=${USE_OPENTELEMETRY-true} 34 | - SampleApp__ApplicationInsightsInstrumentationKey=${AI_INSTRUMENTATIONKEY-} 35 | - SampleApp__ApplicationInsightsForOpenTelemetryInstrumentationKey=${AI_INSTRUMENTATIONKEY_OPENTELEMETRY-} 36 | - OpenTelemetry__Jaeger__AgentHost=${JAEGER_AGENTHOST-jaeger} 37 | depends_on: 38 | - rabbitmq 39 | - sample.timeapi 40 | - prometheus 41 | - jaeger 42 | build: 43 | context: . 44 | dockerfile: src/Sample.RabbitMQProcessor/Dockerfile 45 | 46 | sample.timeapi: 47 | image: ${DOCKER_REGISTRY-}dotnetobservabilitysample-timeapi:${TAG-latest} 48 | environment: 49 | - SampleApp__RabbitMQHostName=rabbitmq 50 | - SampleApp__TimeAPIUrl=http://sample.timeapi 51 | - SampleApp__UseApplicationInsights=${USE_APPLICATIONINSIGHTS-false} 52 | - SampleApp__UseOpenTelemetry=${USE_OPENTELEMETRY-true} 53 | - SampleApp__ApplicationInsightsInstrumentationKey=${AI_INSTRUMENTATIONKEY-} 54 | - SampleApp__ApplicationInsightsForOpenTelemetryInstrumentationKey=${AI_INSTRUMENTATIONKEY_OPENTELEMETRY-} 55 | - SqlConnectionString=server=sqlserver;user id=sa;password=Pass@Word1; 56 | - OpenTelemetry__Jaeger__AgentHost=${JAEGER_AGENTHOST-jaeger} 57 | depends_on: 58 | - sqlserver 59 | - prometheus 60 | - jaeger 61 | build: 62 | context: . 63 | dockerfile: src/Sample.TimeApi/Dockerfile 64 | ports: 65 | - "5002:80" 66 | 67 | rabbitmq: 68 | image: rabbitmq:3-management 69 | ports: 70 | - 15672 71 | - 5672 72 | 73 | sqlserver: 74 | image: mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 75 | environment: 76 | - ACCEPT_EULA=Y 77 | - SA_PASSWORD=Pass@Word1 78 | ports: 79 | - 1433 80 | 81 | jaeger: 82 | image: jaegertracing/all-in-one 83 | environment: 84 | - COLLECTOR_ZIPKIN_HTTP_PORT=19411 85 | ports: 86 | - 5775:5775/udp 87 | - 6831:6831/udp 88 | - 6832:6832/udp 89 | - 5778:5778 90 | - 16686:16686 91 | - 14268:14268 92 | - 19411:19411 93 | 94 | prometheus: 95 | image: prom/prometheus 96 | volumes: 97 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 98 | command: 99 | - '--config.file=/etc/prometheus/prometheus.yml' 100 | ports: 101 | - 9090:9090 102 | grafana: 103 | image: grafana/grafana 104 | environment: 105 | - GF_SECURITY_ADMIN_PASSWORD=password1 106 | depends_on: 107 | - prometheus 108 | ports: 109 | - 3000:3000 110 | -------------------------------------------------------------------------------- /media/ai-search-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/application-insights-aspnet-sample-opentelemetry/13c48d0ac4ae15ff96671b735443f6a94310f4ec/media/ai-search-logs.png -------------------------------------------------------------------------------- /media/ai-tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/application-insights-aspnet-sample-opentelemetry/13c48d0ac4ae15ff96671b735443f6a94310f4ec/media/ai-tracing.png -------------------------------------------------------------------------------- /media/grafana-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/application-insights-aspnet-sample-opentelemetry/13c48d0ac4ae15ff96671b735443f6a94310f4ec/media/grafana-metrics.png -------------------------------------------------------------------------------- /media/jaeger-tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/application-insights-aspnet-sample-opentelemetry/13c48d0ac4ae15ff96671b735443f6a94310f4ec/media/jaeger-tracing.png -------------------------------------------------------------------------------- /media/sample-app-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/application-insights-aspnet-sample-opentelemetry/13c48d0ac4ae15ff96671b735443f6a94310f4ec/media/sample-app-overview.png -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | # prometheus.yml 2 | global: 3 | scrape_interval: 5s 4 | external_labels: 5 | monitor: 'dotnet-observability-sample' 6 | 7 | 8 | scrape_configs: 9 | - job_name: 'prometheus' 10 | scrape_interval: 5s 11 | static_configs: 12 | - targets: ['localhost:9090'] 13 | 14 | - job_name: 'main-api' 15 | scrape_interval: 5s 16 | static_configs: 17 | - targets: ['sample.mainapi:9184'] 18 | 19 | - job_name: 'rabbitmq-processor' 20 | scrape_interval: 5s 21 | static_configs: 22 | - targets: ['sample.rabbitmqprocessor:9185'] -------------------------------------------------------------------------------- /quickstart/prometheus-grafana/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '2' 3 | services: 4 | prometheus: 5 | image: prom/prometheus 6 | volumes: 7 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 8 | command: 9 | - '--config.file=/etc/prometheus/prometheus.yml' 10 | ports: 11 | - 9090:9090 12 | grafana: 13 | image: grafana/grafana 14 | environment: 15 | - GF_SECURITY_ADMIN_PASSWORD=password1 16 | depends_on: 17 | - prometheus 18 | ports: 19 | - 3000:3000 -------------------------------------------------------------------------------- /quickstart/prometheus-grafana/prometheus.yml: -------------------------------------------------------------------------------- 1 | # prometheus.yml 2 | global: 3 | scrape_interval: 5s 4 | external_labels: 5 | monitor: 'dotnet-observability-sample' 6 | 7 | 8 | scrape_configs: 9 | - job_name: 'prometheus' 10 | scrape_interval: 5s 11 | static_configs: 12 | - targets: ['localhost:9090'] 13 | 14 | - job_name: 'main-api' 15 | scrape_interval: 5s 16 | static_configs: 17 | - targets: ['host.docker.internal:9184'] 18 | 19 | - job_name: 'rabbitmq-processor' 20 | scrape_interval: 5s 21 | static_configs: 22 | - targets: ['host.docker.internal:9185'] 23 | -------------------------------------------------------------------------------- /quickstart/sample/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | sample.mainapi: 5 | image: fbeltrao/dotnetobservabilitysample-mainapi 6 | environment: 7 | - SampleApp__RabbitMQHostName=rabbitmq 8 | - SampleApp__TimeAPIUrl=http://sample.timeapi 9 | - SampleApp__UseApplicationInsights=${USE_APPLICATIONINSIGHTS-false} 10 | - SampleApp__UseOpenTelemetry=${USE_OPENTELEMETRY-true} 11 | - SampleApp__ApplicationInsightsInstrumentationKey=${AI_INSTRUMENTATIONKEY-} 12 | - SampleApp__ApplicationInsightsForOpenTelemetryInstrumentationKey=${AI_INSTRUMENTATIONKEY_OPENTELEMETRY-} 13 | - OpenTelemetry__Prometheus__Url=http://sample.mainapi:9184/metrics/ 14 | - OpenTelemetry__Jaeger__AgentHost=${JAEGER_AGENTHOST-jaeger} 15 | depends_on: 16 | - rabbitmq 17 | - sample.timeapi 18 | - prometheus 19 | - jaeger 20 | ports: 21 | - "5001:80" 22 | - "9184:9184" 23 | 24 | sample.rabbitmqprocessor: 25 | image: fbeltrao/dotnetobservabilitysample-rabbitmqprocessor 26 | environment: 27 | - SampleApp__RabbitMQHostName=rabbitmq 28 | - SampleApp__TimeAPIUrl=http://sample.timeapi 29 | - SampleApp__UseApplicationInsights=${USE_APPLICATIONINSIGHTS-false} 30 | - SampleApp__UseOpenTelemetry=${USE_OPENTELEMETRY-true} 31 | - SampleApp__ApplicationInsightsInstrumentationKey=${AI_INSTRUMENTATIONKEY-} 32 | - SampleApp__ApplicationInsightsForOpenTelemetryInstrumentationKey=${AI_INSTRUMENTATIONKEY_OPENTELEMETRY-} 33 | - OpenTelemetry__Prometheus__Url=http://sample.rabbitmqprocessor:9185/metrics/ 34 | - OpenTelemetry__Jaeger__AgentHost=${JAEGER_AGENTHOST-jaeger} 35 | depends_on: 36 | - rabbitmq 37 | - sample.timeapi 38 | - prometheus 39 | - jaeger 40 | ports: 41 | - "9185:9185" 42 | 43 | sample.timeapi: 44 | image: fbeltrao/dotnetobservabilitysample-timeapi 45 | environment: 46 | - SampleApp__RabbitMQHostName=rabbitmq 47 | - SampleApp__UseApplicationInsights=${USE_APPLICATIONINSIGHTS-false} 48 | - SampleApp__UseOpenTelemetry=${USE_OPENTELEMETRY-true} 49 | - SampleApp__ApplicationInsightsInstrumentationKey=${AI_INSTRUMENTATIONKEY-} 50 | - SampleApp__ApplicationInsightsForOpenTelemetryInstrumentationKey=${AI_INSTRUMENTATIONKEY_OPENTELEMETRY-} 51 | - SqlConnectionString=server=sqlserver;user id=sa;password=Pass@Word1; 52 | - OpenTelemetry__Jaeger__AgentHost=${JAEGER_AGENTHOST-jaeger} 53 | depends_on: 54 | - sqlserver 55 | - prometheus 56 | - jaeger 57 | ports: 58 | - "5002:80" 59 | 60 | rabbitmq: 61 | image: rabbitmq:3-management 62 | ports: 63 | - 15672 64 | - 5672 65 | 66 | sqlserver: 67 | image: mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 68 | environment: 69 | - ACCEPT_EULA=Y 70 | - SA_PASSWORD=Pass@Word1 71 | ports: 72 | - 1433 73 | 74 | jaeger: 75 | image: jaegertracing/all-in-one 76 | environment: 77 | - COLLECTOR_ZIPKIN_HTTP_PORT=19411 78 | ports: 79 | - 5775:5775/udp 80 | - 6831:6831/udp 81 | - 6832:6832/udp 82 | - 5778:5778 83 | - 16686:16686 84 | - 14268:14268 85 | - 19411:19411 86 | 87 | prometheus: 88 | image: prom/prometheus 89 | volumes: 90 | - ../../prometheus.yml:/etc/prometheus/prometheus.yml 91 | command: 92 | - '--config.file=/etc/prometheus/prometheus.yml' 93 | ports: 94 | - 9090:9090 95 | grafana: 96 | image: grafana/grafana 97 | environment: 98 | - GF_SECURITY_ADMIN_PASSWORD=password1 99 | depends_on: 100 | - prometheus 101 | ports: 102 | - 3000:3000 103 | -------------------------------------------------------------------------------- /src/Sample.Common/ApplicationInformation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Sample.Common 5 | { 6 | public static class ApplicationInformation 7 | { 8 | static ApplicationInformation() 9 | { 10 | var assemblyName = Assembly.GetEntryAssembly().GetName(); 11 | Name = assemblyName.Name.ToLowerInvariant(); 12 | Version = assemblyName.Version; 13 | } 14 | 15 | public static string Name { get; } 16 | public static Version Version { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Sample.Common/ApplicationInsightsLink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using OpenTelemetry.Trace; 4 | 5 | namespace Sample.Common 6 | { 7 | public class ApplicationInsightsLink 8 | { 9 | public const string TelemetryPropertyName = "_MS.links"; 10 | 11 | [System.Text.Json.Serialization.JsonPropertyName("operation_Id")] 12 | public string OperationId { get; set; } 13 | 14 | [System.Text.Json.Serialization.JsonPropertyName("id")] 15 | public string Id { get; set; } 16 | 17 | public ApplicationInsightsLink() 18 | { 19 | } 20 | 21 | public ApplicationInsightsLink(Activity activity) 22 | { 23 | if (activity is null) 24 | { 25 | throw new System.ArgumentNullException(nameof(activity)); 26 | } 27 | 28 | this.OperationId = activity.TraceId.ToString(); 29 | this.Id = activity.Id.ToString(); 30 | } 31 | 32 | public ApplicationInsightsLink(SpanContext spanContext) 33 | { 34 | if (!spanContext.IsValid) 35 | { 36 | throw new ArgumentException("Invalid span context", nameof(spanContext)); 37 | } 38 | 39 | this.OperationId = spanContext.TraceId.ToString(); 40 | this.Id = spanContext.SpanId.ToString(); 41 | } 42 | 43 | public ApplicationInsightsLink(ActivityTraceId traceId, ActivitySpanId spanId) 44 | { 45 | this.OperationId = traceId.ToString(); 46 | this.Id = spanId.ToString(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Sample.Common/CloudRoleTelemetryInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.ApplicationInsights.Channel; 4 | using Microsoft.ApplicationInsights.Extensibility; 5 | 6 | namespace Sample.Common 7 | { 8 | internal class CloudRoleTelemetryInitializer : ITelemetryInitializer 9 | { 10 | private readonly string roleName; 11 | private readonly string roleInstance; 12 | private readonly string version; 13 | 14 | public CloudRoleTelemetryInitializer() 15 | { 16 | var name = Assembly.GetEntryAssembly().GetName(); 17 | this.roleName = name.Name; 18 | this.roleInstance = Environment.MachineName; 19 | this.version = name.Version.ToString(); 20 | } 21 | 22 | public void Initialize(ITelemetry telemetry) 23 | { 24 | telemetry.Context.Cloud.RoleName = roleName; 25 | telemetry.Context.Cloud.RoleInstance = roleInstance; 26 | telemetry.Context.GlobalProperties["AppVersion"] = version; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Sample.Common/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Sample.Common 5 | { 6 | 7 | public static class ConfigurationExtensions 8 | { 9 | const string SampleAppOptionsConfigSection = "SampleApp"; 10 | 11 | public static IServiceCollection AddSampleAppOptions(this IServiceCollection services, IConfiguration configuration) 12 | { 13 | return services.Configure(configuration.GetSection(SampleAppOptionsConfigSection)); 14 | } 15 | 16 | public static SampleAppOptions GetSampleAppOptions(this IConfiguration configuration) 17 | { 18 | var telemetryOptions = new SampleAppOptions(); 19 | configuration.GetSection(SampleAppOptionsConfigSection).Bind(telemetryOptions); 20 | return telemetryOptions; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Sample.Common/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Common 2 | { 3 | public static class Constants 4 | { 5 | public const string FirstQueueName = "sample_telemetry"; 6 | public const string WebQueueName = "sample_web"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Sample.Common/EnqueuedMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Sample.Common 4 | { 5 | public class EnqueuedMessage 6 | { 7 | [JsonPropertyName("day")] 8 | public string Day { get; set; } 9 | 10 | [JsonPropertyName("eventName")] 11 | public string EventName { get; set; } 12 | 13 | [JsonPropertyName("source")] 14 | public string Source { get; set; } 15 | 16 | public EnqueuedMessage() 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Sample.Common/FailGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Sample.Common 3 | { 4 | public static class FailGenerator 5 | { 6 | static Random random = new Random(); 7 | 8 | public static void FailIfNeeded(int failRate) 9 | { 10 | var v = 0; 11 | lock (random) 12 | { 13 | // between 0 and 99 14 | v = random.Next(100); 15 | } 16 | 17 | v++; 18 | 19 | if (v <= failRate) 20 | { 21 | throw new GeneratedFailureException($"Failed ({failRate}% chance)"); 22 | } 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Sample.Common/GeneratedFailureException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Sample.Common 5 | { 6 | [Serializable] 7 | internal class GeneratedFailureException : Exception 8 | { 9 | public GeneratedFailureException() 10 | { 11 | } 12 | 13 | public GeneratedFailureException(string message) : base(message) 14 | { 15 | } 16 | 17 | public GeneratedFailureException(string message, Exception innerException) : base(message, innerException) 18 | { 19 | } 20 | 21 | protected GeneratedFailureException(SerializationInfo info, StreamingContext context) : base(info, context) 22 | { 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Sample.Common/IAppMetrics.cs: -------------------------------------------------------------------------------- 1 | using OpenTelemetry.Metrics.Configuration; 2 | 3 | namespace Sample.Common 4 | { 5 | public interface IAppMetrics 6 | { 7 | void Initialize(MeterFactory meterFactory); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Sample.Common/OpenTelemetryExtensions.cs: -------------------------------------------------------------------------------- 1 | using OpenTelemetry.Trace; 2 | 3 | namespace Sample.Common 4 | { 5 | 6 | public static class OpenTelemetryExtensions 7 | { 8 | public static string TracerServiceName { get; } 9 | 10 | private static readonly string appTracerVersion; 11 | 12 | static OpenTelemetryExtensions() 13 | { 14 | TracerServiceName = ApplicationInformation.Name.ToLowerInvariant(); 15 | appTracerVersion = $"semver:{ApplicationInformation.Version.ToString()}"; 16 | } 17 | 18 | public static Tracer GetApplicationTracer(this TracerFactoryBase tracerFactory) 19 | { 20 | return tracerFactory.GetTracer(TracerServiceName, appTracerVersion); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Sample.Common/PromotheusExporterHostedService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using OpenTelemetry.Exporter.Prometheus; 7 | using OpenTelemetry.Metrics; 8 | using OpenTelemetry.Metrics.Configuration; 9 | using OpenTelemetry.Metrics.Export; 10 | 11 | namespace Sample.Common 12 | { 13 | public class PromotheusExporterHostedService : IHostedService 14 | { 15 | private readonly PrometheusExporter exporter; 16 | private readonly IEnumerable initializers; 17 | private Timer timer; 18 | private MeterFactory meterFactory; 19 | 20 | public PromotheusExporterHostedService(PrometheusExporter exporter, IEnumerable initializers) 21 | { 22 | this.exporter = exporter ?? throw new System.ArgumentNullException(nameof(exporter)); 23 | this.initializers = initializers; 24 | } 25 | 26 | public Task StartAsync(CancellationToken cancellationToken) 27 | { 28 | var interval = TimeSpan.FromSeconds(5); 29 | var simpleProcessor = new UngroupedBatcher(exporter, interval); 30 | this.meterFactory = MeterFactory.Create(simpleProcessor); 31 | 32 | foreach (var initializer in initializers) 33 | { 34 | initializer.Initialize(meterFactory); 35 | } 36 | 37 | this.timer = new Timer(CollectMetrics, meterFactory, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); 38 | 39 | exporter.Start(); 40 | 41 | this.timer.Change(interval, interval); 42 | 43 | return Task.CompletedTask; 44 | } 45 | 46 | 47 | /// 48 | /// Need to dig deeper into this 49 | /// This call should not be needed 50 | /// 51 | /// 52 | private static void CollectMetrics(object state) 53 | { 54 | var meterFactory = (MeterFactory)state; 55 | var m = meterFactory.GetMeter("Sample App"); 56 | ((MeterSdk)m).Collect(); 57 | } 58 | 59 | public Task StopAsync(CancellationToken cancellationToken) 60 | { 61 | exporter.Stop(); 62 | timer.Dispose(); 63 | return Task.CompletedTask; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/Sample.Common/Sample.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 0.2 6 | 8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Sample.Common/SampleAppOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Common 2 | { 3 | public class SampleAppOptions 4 | { 5 | public string RabbitMQHostName { get; set; } = "localhost"; 6 | public string TimeAPIUrl { get; set; } = "http://localhost:5002"; 7 | public bool UseOpenTelemetry { get; set; } 8 | public bool UseApplicationInsights { get; set; } 9 | 10 | public string ApplicationInsightsInstrumentationKey { get; set; } 11 | public string ApplicationInsightsForOpenTelemetryInstrumentationKey { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Sample.Common/SampleServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using OpenTelemetry.Trace.Configuration; 6 | using OpenTelemetry.Trace.Samplers; 7 | using System.Reflection; 8 | using OpenTelemetry.Resources; 9 | using System.Collections.Generic; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.ApplicationInsights.Extensibility; 12 | using System.IO; 13 | using OpenTelemetry.Exporter.Prometheus; 14 | using OpenTelemetry.Exporter.Jaeger; 15 | using OpenTelemetry.Exporter.Zipkin; 16 | using Microsoft.Extensions.Options; 17 | 18 | namespace Sample.Common 19 | { 20 | public static class SampleServiceCollectionExtensions 21 | { 22 | public static IServiceCollection AddWebSampleTelemetry(this IServiceCollection services, IConfiguration configuration, Action traceBuilder = null) 23 | { 24 | var sampleAppOptions = configuration.GetSampleAppOptions(); 25 | 26 | if (sampleAppOptions.UseOpenTelemetry) 27 | services.AddSampleOpenTelemetry(sampleAppOptions, configuration, traceBuilder); 28 | 29 | if (sampleAppOptions.UseApplicationInsights) 30 | services.AddSampleApplicationInsights(isWeb: true, sampleAppOptions, configuration); 31 | 32 | return services; 33 | } 34 | 35 | public static IServiceCollection AddWorkerSampleTelemetry(this IServiceCollection services, IConfiguration configuration) 36 | { 37 | var telemetryOptions = configuration.GetSampleAppOptions(); 38 | 39 | if (telemetryOptions.UseOpenTelemetry) 40 | services.AddSampleOpenTelemetry(telemetryOptions, configuration); 41 | 42 | if (telemetryOptions.UseApplicationInsights) 43 | services.AddSampleApplicationInsights(isWeb: false, telemetryOptions, configuration); 44 | 45 | return services; 46 | } 47 | 48 | 49 | static IServiceCollection AddSampleOpenTelemetry(this IServiceCollection services, SampleAppOptions sampleAppOptions, IConfiguration configuration, Action traceBuilder = null) 50 | { 51 | var openTelemetryConfigSection = configuration.GetSection("OpenTelemetry"); 52 | var jaegerConfigSection = openTelemetryConfigSection.GetSection("Jaeger"); 53 | services.Configure(jaegerConfigSection); 54 | 55 | var zipkinConfigSection = openTelemetryConfigSection.GetSection("Zipkin"); 56 | services.Configure(zipkinConfigSection); 57 | 58 | // setup open telemetry 59 | services.AddOpenTelemetry((sp, builder) => 60 | { 61 | var serviceName = OpenTelemetryExtensions.TracerServiceName; 62 | 63 | var exporterCount = 0; 64 | 65 | if (zipkinConfigSection.Exists()) 66 | { 67 | var zipkinOptions = sp.GetService>(); 68 | if (zipkinOptions.Value != null && zipkinOptions.Value.Endpoint != null) 69 | { 70 | // To start zipkin: 71 | // docker run -d -p 9411:9411 openzipkin/zipkin 72 | exporterCount++; 73 | 74 | builder.UseZipkin(o => 75 | { 76 | o.Endpoint = zipkinOptions.Value.Endpoint; 77 | o.ServiceName = serviceName; 78 | }); 79 | 80 | Console.WriteLine("Using OpenTelemetry Zipkin exporter"); 81 | } 82 | } 83 | 84 | 85 | if (!string.IsNullOrWhiteSpace(sampleAppOptions.ApplicationInsightsForOpenTelemetryInstrumentationKey)) 86 | { 87 | exporterCount++; 88 | 89 | builder.UseApplicationInsights(o => 90 | { 91 | o.InstrumentationKey = sampleAppOptions.ApplicationInsightsForOpenTelemetryInstrumentationKey; 92 | o.TelemetryInitializers.Add(new CloudRoleTelemetryInitializer()); 93 | }); 94 | 95 | Console.WriteLine("Using OpenTelemetry ApplicationInsights exporter"); 96 | } 97 | 98 | if (jaegerConfigSection.Exists()) 99 | { 100 | // Running jaeger with docker 101 | // docker run -d --name jaeger \ 102 | // -e COLLECTOR_ZIPKIN_HTTP_PORT=19411 \ 103 | // -p 5775:5775/udp \ 104 | // -p 6831:6831/udp \ 105 | // -p 6832:6832/udp \ 106 | // -p 5778:5778 \ 107 | // -p 16686:16686 \ 108 | // -p 14268:14268 \ 109 | // -p 19411:19411 \ 110 | // jaegertracing/all-in-one 111 | var jaegerOptions = sp.GetService>(); 112 | if (jaegerOptions.Value != null && !string.IsNullOrWhiteSpace(jaegerOptions.Value.AgentHost)) 113 | { 114 | exporterCount++; 115 | 116 | builder.UseJaeger(o => 117 | { 118 | o.ServiceName = serviceName; 119 | o.AgentHost = jaegerOptions.Value.AgentHost; 120 | o.AgentPort = jaegerOptions.Value.AgentPort; 121 | o.MaxPacketSize = jaegerOptions.Value.MaxPacketSize; 122 | o.ProcessTags = jaegerOptions.Value.ProcessTags; 123 | }); 124 | 125 | Console.WriteLine("Using OpenTelemetry Jaeger exporter"); 126 | } 127 | } 128 | 129 | if (exporterCount == 0) 130 | { 131 | throw new Exception("No sink for open telemetry was configured"); 132 | } 133 | 134 | builder 135 | .SetSampler(new AlwaysSampleSampler()) 136 | .AddDependencyCollector(config => 137 | { 138 | config.SetHttpFlavor = true; 139 | }) 140 | .AddRequestCollector() 141 | .SetResource(new Resource(new Dictionary 142 | { 143 | { "service.name", serviceName } 144 | })); 145 | 146 | traceBuilder?.Invoke(builder); 147 | }); 148 | 149 | 150 | var prometheusConfigSection = openTelemetryConfigSection.GetSection("Prometheus"); 151 | if (prometheusConfigSection.Exists()) 152 | { 153 | var prometheusExporterOptions = new PrometheusExporterOptions(); 154 | prometheusConfigSection.Bind(prometheusExporterOptions); 155 | 156 | if (!string.IsNullOrWhiteSpace(prometheusExporterOptions.Url)) 157 | { 158 | var prometheusExporter = new PrometheusExporter(prometheusExporterOptions); 159 | services.AddSingleton(prometheusExporter); 160 | 161 | // Add start/stop lifetime support 162 | services.AddHostedService(); 163 | 164 | Console.WriteLine($"Using OpenTelemetry Prometheus exporter in '{prometheusExporterOptions.Url}'"); 165 | } 166 | } 167 | 168 | return services; 169 | } 170 | 171 | static IServiceCollection AddSampleApplicationInsights(this IServiceCollection services, bool isWeb, SampleAppOptions sampleAppOptions, IConfiguration configuration) 172 | { 173 | if (isWeb) 174 | { 175 | services.AddApplicationInsightsTelemetry(o => 176 | { 177 | o.InstrumentationKey = sampleAppOptions.ApplicationInsightsInstrumentationKey; 178 | o.ApplicationVersion = ApplicationInformation.Version.ToString(); 179 | }); 180 | } 181 | else 182 | { 183 | services.AddApplicationInsightsTelemetryWorkerService(o => 184 | { 185 | o.InstrumentationKey = sampleAppOptions.ApplicationInsightsInstrumentationKey; 186 | o.ApplicationVersion = ApplicationInformation.Version.ToString(); 187 | }); 188 | } 189 | 190 | services.AddSingleton(); 191 | 192 | Console.WriteLine("Using Application Insights SDK"); 193 | 194 | return services; 195 | } 196 | 197 | public static void ConfigureLogging(HostBuilderContext hostBuilderContext, ILoggingBuilder loggingBuilder) 198 | { 199 | var telemetryOptions = hostBuilderContext.Configuration.GetSampleAppOptions(); 200 | 201 | if (telemetryOptions.UseApplicationInsights) 202 | { 203 | loggingBuilder.AddApplicationInsights(telemetryOptions.ApplicationInsightsInstrumentationKey); 204 | } 205 | 206 | loggingBuilder.AddConsole((options) => { options.IncludeScopes = true; }); 207 | } 208 | 209 | public static void ConfigureAppConfiguration(IConfigurationBuilder builder, string[] args, Assembly mainAssembly) 210 | { 211 | builder.SetBasePath(Directory.GetCurrentDirectory()); 212 | builder.AddJsonFile("appsettings.json", optional: true); 213 | builder.AddEnvironmentVariables(); 214 | 215 | #if DEBUG 216 | // Needed to add this when using a shared file when debugging 217 | // It tries to get from the directory where the project is 218 | //var path = Path.GetDirectoryName(mainAssembly.Location); 219 | //var envJsonFile = Path.Combine(path, $"appsettings.Development.json"); 220 | builder.AddJsonFile("appsettings.Development.json", optional: true); 221 | #endif 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Controllers/MainController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Logging; 6 | using OpenTelemetry.Trace; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Sample.Common; 9 | using System.Diagnostics; 10 | using Microsoft.ApplicationInsights; 11 | using System.Threading.Channels; 12 | using System.Threading; 13 | using Microsoft.Extensions.Options; 14 | 15 | namespace Sample.MainApi.Controllers 16 | { 17 | [ApiController] 18 | [Route("api")] 19 | public class MainController : ControllerBase 20 | { 21 | private readonly IHttpClientFactory httpClientFactory; 22 | private readonly string timeApiUrl; 23 | private readonly ILogger logger; 24 | private readonly Metrics metrics; 25 | private readonly ChannelWriter channelWriter; 26 | private readonly Tracer tracer; 27 | private readonly TelemetryClient telemetryClient; 28 | 29 | public MainController(IOptions sampleAppOptions, 30 | IHttpClientFactory httpClientFactory, 31 | ILogger logger, 32 | IServiceProvider serviceProvider, 33 | Metrics metrics, 34 | ChannelWriter channelWriter) 35 | { 36 | this.timeApiUrl = sampleAppOptions.Value.TimeAPIUrl; 37 | this.httpClientFactory = httpClientFactory; 38 | this.logger = logger; 39 | this.metrics = metrics; 40 | this.channelWriter = channelWriter; 41 | var tracerFactory = serviceProvider.GetService(); 42 | this.tracer = tracerFactory?.GetApplicationTracer(); 43 | 44 | this.telemetryClient = serviceProvider.GetService(); 45 | } 46 | 47 | [HttpGet("enqueue/{source}")] 48 | public async Task EnqueueAsync( 49 | [FromServices]IRabbitMQProducer rabbitMQProducer, // Using FromServices to allow lazy creation of RabbitMQ connection 50 | string source, 51 | string eventName = null) 52 | { 53 | await Task.Delay(100); 54 | 55 | FailGenerator.FailIfNeeded(1); 56 | 57 | var apiFullUrl = $"{timeApiUrl}/api/time/localday"; 58 | if (logger.IsEnabled(LogLevel.Debug)) 59 | { 60 | logger.LogDebug("Getting time from {url}", apiFullUrl); 61 | } 62 | 63 | var day = await httpClientFactory.CreateClient().GetStringAsync(apiFullUrl); 64 | 65 | var jsonResponse = new EnqueuedMessage { Day = day, EventName = eventName, Source = source ?? "N/a" }; 66 | var message = System.Text.Json.JsonSerializer.Serialize(jsonResponse); 67 | 68 | rabbitMQProducer.Publish(message); 69 | 70 | metrics.TrackItemEnqueued(1, source); 71 | 72 | return new JsonResult(jsonResponse); 73 | } 74 | 75 | 76 | [HttpGet("dbtime")] 77 | public async Task GetDbTimeAsync() 78 | { 79 | await Task.Delay(100); 80 | 81 | FailGenerator.FailIfNeeded(1); 82 | 83 | var apiFullUrl = $"{timeApiUrl}/api/time/dbtime"; 84 | return await httpClientFactory.CreateClient().GetStringAsync(apiFullUrl); 85 | } 86 | 87 | [HttpGet("referenceLinks")] 88 | public async Task ReferenceLinksExample(CancellationToken cancellationToken) 89 | { 90 | var req = new HelloRequest 91 | { 92 | Cities = new[] { "Zurich", "Seattle", "London" }, 93 | ParentId = Activity.Current.SpanId, 94 | TraceId = Activity.Current.TraceId, 95 | RequestTime = DateTime.UtcNow, 96 | }; 97 | 98 | await channelWriter.WriteAsync(req, cancellationToken); 99 | 100 | return $"Queued as {req.RequestTime}"; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 8 | WORKDIR /src 9 | COPY ["src/Sample.Common/Sample.Common.csproj", "src/Sample.Common/"] 10 | COPY ["src/Sample.RabbitMQCollector/Sample.RabbitMQCollector.csproj", "src/Sample.RabbitMQCollector/"] 11 | COPY ["NuGet.config", "./"] 12 | COPY ["src/Sample.MainApi/Sample.MainApi.csproj", "src/Sample.MainApi/"] 13 | 14 | RUN dotnet restore "src/Sample.MainApi/Sample.MainApi.csproj" 15 | COPY . . 16 | WORKDIR "/src/src/Sample.MainApi" 17 | RUN dotnet build "Sample.MainApi.csproj" -c Release -o /app/build 18 | 19 | FROM build AS publish 20 | RUN dotnet publish "Sample.MainApi.csproj" -c Release -o /app/publish 21 | 22 | FROM base AS final 23 | WORKDIR /app 24 | COPY --from=publish /app/publish . 25 | ENTRYPOINT ["dotnet", "Sample.MainApi.dll"] -------------------------------------------------------------------------------- /src/Sample.MainApi/HelloRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Collections.Generic; 4 | 5 | namespace Sample.MainApi 6 | { 7 | public class HelloRequest 8 | { 9 | public DateTime RequestTime { get; set; } 10 | public IEnumerable Cities { get; set; } 11 | public ActivitySpanId ParentId { get; set; } 12 | public ActivityTraceId TraceId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Sample.MainApi/HostedServices/HelloHostedService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using OpenTelemetry.Trace; 7 | using Sample.Common; 8 | using Microsoft.ApplicationInsights; 9 | using System.Threading.Channels; 10 | using System.Diagnostics; 11 | using Microsoft.ApplicationInsights.DataContracts; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | 15 | namespace Sample.MainApi.HostedServices 16 | { 17 | public class HelloHostedService : IHostedService 18 | { 19 | private readonly Tracer tracer; 20 | private readonly TelemetryClient telemetryClient; 21 | CancellationTokenSource cts; 22 | Task pendingTask; 23 | private readonly ChannelReader channelReader; 24 | 25 | public HelloHostedService(IServiceProvider serviceProvider, ChannelReader channelReader) 26 | { 27 | var tracerFactory = serviceProvider.GetService(); 28 | this.tracer = tracerFactory?.GetApplicationTracer(); 29 | 30 | this.telemetryClient = serviceProvider.GetService(); 31 | cts = new CancellationTokenSource(); 32 | 33 | this.channelReader = channelReader; 34 | } 35 | 36 | public Task StartAsync(CancellationToken cancellationToken) 37 | { 38 | pendingTask = Task.Factory.StartNew(() => Processor(cts.Token), TaskCreationOptions.LongRunning); 39 | return Task.CompletedTask; 40 | } 41 | 42 | async Task ProcessItem(HelloRequest request) 43 | { 44 | async Task<(Activity Activity, string Message)> OpenTelemetrySayHello(DateTime start, string city) 45 | { 46 | var res = await RawSayHello(start, city); 47 | var span = tracer.StartSpanFromActivity(res.Activity.OperationName, res.Activity, SpanKind.Consumer); 48 | 49 | span.End(); 50 | 51 | return res; 52 | } 53 | 54 | async Task<(Activity Activity, string Message)> ApplicationInsightsSayHello(DateTime start, string city) 55 | { 56 | var res = await RawSayHello(start, city); 57 | var operation = telemetryClient.StartOperation(res.Activity.OperationName, res.Activity.TraceId.ToString(), res.Activity.SpanId.ToString()); 58 | 59 | telemetryClient.StopOperation(operation); 60 | 61 | return res; 62 | } 63 | 64 | async Task<(Activity Activity, string Message)> RawSayHello(DateTime start, string city) 65 | { 66 | var activity = new Activity("Single Say Hello").Start(); 67 | activity.AddBaggage("city", city); 68 | await Task.Delay(10); 69 | return (activity, $"{start}: Hello {city}"); 70 | } 71 | 72 | Func> runner = null; 73 | 74 | if (tracer != null) 75 | runner = OpenTelemetrySayHello; 76 | else if (telemetryClient != null) 77 | runner = ApplicationInsightsSayHello; 78 | 79 | if (runner == null) 80 | return; 81 | 82 | var batchStart = DateTimeOffset.UtcNow; 83 | var tasks = new List>(); 84 | foreach (var v in request.Cities) 85 | { 86 | tasks.Add(Task.Run(() => runner(request.RequestTime, v))); 87 | } 88 | 89 | 90 | await Task.WhenAll(tasks); 91 | 92 | if (this.tracer != null) 93 | { 94 | var opts = new SpanCreationOptions 95 | { 96 | StartTimestamp = batchStart, 97 | Links = tasks.Select(x => new Link(ExtractContext(x.Result.Activity))), 98 | }; 99 | 100 | tracer.StartActiveSpan("Say Hello batch processing", SpanKind.Consumer, opts, out var batchSpan); 101 | batchSpan.End(); 102 | } 103 | else if (telemetryClient != null) 104 | { 105 | var links = tasks.Select(x => new ApplicationInsightsLink(x.Result.Activity)); 106 | using var batchOperation = telemetryClient.StartOperation("Say Hello batch processing"); 107 | 108 | batchOperation.Telemetry.Timestamp = batchStart; 109 | 110 | batchOperation.Telemetry.Properties[ApplicationInsightsLink.TelemetryPropertyName] = System.Text.Json.JsonSerializer.Serialize(links); 111 | } 112 | } 113 | 114 | async Task Processor(CancellationToken cancellationToken) 115 | { 116 | while (true) 117 | { 118 | var req = await channelReader.ReadAsync(cancellationToken); 119 | await ProcessItem(req); 120 | } 121 | } 122 | 123 | public async Task StopAsync(CancellationToken cancellationToken) 124 | { 125 | cts.Cancel(); 126 | if (pendingTask != null) 127 | await pendingTask; 128 | } 129 | 130 | private SpanContext ExtractContext(Activity activity) 131 | { 132 | return new SpanContext(activity.TraceId, activity.SpanId, activity.ActivityTraceFlags); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Sample.MainApi/IRabbitMQProducer.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.MainApi 2 | { 3 | public interface IRabbitMQProducer 4 | { 5 | string HostName { get; } 6 | string QueueName { get; } 7 | 8 | void Publish(string message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Metrics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.ApplicationInsights; 4 | using Microsoft.ApplicationInsights.Metrics; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using OpenTelemetry.Metrics; 7 | using OpenTelemetry.Metrics.Configuration; 8 | using OpenTelemetry.Trace; 9 | using Sample.Common; 10 | 11 | namespace Sample.MainApi 12 | { 13 | public class Metrics : IAppMetrics 14 | { 15 | private readonly Metric appInsightsItemEnqueuedCounter; 16 | private Meter meter; 17 | private Counter openTelemetryItemEnqueuedCounter; 18 | 19 | public Metrics(IServiceProvider serviceProvider) 20 | { 21 | var telemetryClient = serviceProvider.GetService(); 22 | if (telemetryClient != null) 23 | { 24 | this.appInsightsItemEnqueuedCounter = telemetryClient.GetMetric(new MetricIdentifier("Sample App", "Enqueued Item", "Source")); 25 | } 26 | } 27 | 28 | void IAppMetrics.Initialize(MeterFactory meterFactory) 29 | { 30 | this.meter = meterFactory.GetMeter("Sample App"); 31 | this.openTelemetryItemEnqueuedCounter = meter.CreateInt64Counter("Enqueued Item"); 32 | } 33 | 34 | public void TrackItemEnqueued(double metricValue, string source) 35 | { 36 | appInsightsItemEnqueuedCounter?.TrackValue(metricValue, source); 37 | 38 | if (openTelemetryItemEnqueuedCounter != null) 39 | { 40 | var context = default(SpanContext); 41 | var labelSet = new Dictionary() 42 | { 43 | { "Source", source } 44 | }; 45 | 46 | openTelemetryItemEnqueuedCounter.Add(context, 1L, this.meter.GetLabelSet(labelSet)); 47 | 48 | // Collect is called here explicitly as there is 49 | // no controller implementation yet. 50 | // TODO: There should be no need to cast to MeterSdk. 51 | //(meter as MeterSdk).Collect(); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Sample.Common; 5 | 6 | namespace Sample.MainApi 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | Activity.DefaultIdFormat = ActivityIdFormat.W3C; 13 | Activity.ForceDefaultIdFormat = true; 14 | CreateHostBuilder(args).Build().Run(); 15 | } 16 | 17 | public static IHostBuilder CreateHostBuilder(string[] args) => 18 | Host.CreateDefaultBuilder(args) 19 | .ConfigureWebHostDefaults(webBuilder => 20 | { 21 | webBuilder.UseStartup(); 22 | }) 23 | .ConfigureLogging(SampleServiceCollectionExtensions.ConfigureLogging) 24 | .ConfigureAppConfiguration((builder) => SampleServiceCollectionExtensions.ConfigureAppConfiguration(builder, args, typeof(Program).Assembly)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Sample.MainApi": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "api/dbtime", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "applicationUrl": "http://localhost:5001" 11 | }, 12 | "Docker": { 13 | "commandName": "Docker", 14 | "launchBrowser": true, 15 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/dbtime", 16 | "publishAllPorts": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Sample.MainApi/RabbitMQProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Options; 3 | using RabbitMQ.Client; 4 | using Sample.Common; 5 | 6 | namespace Sample.MainApi 7 | { 8 | public class RabbitMQProducer : IRabbitMQProducer, IDisposable 9 | { 10 | public string HostName { get; private set; } 11 | public string QueueName { get; private set; } 12 | 13 | private IConnection connection; 14 | private IModel channel; 15 | 16 | public RabbitMQProducer(IOptions telemetryOptions) 17 | { 18 | HostName = telemetryOptions.Value.RabbitMQHostName; 19 | QueueName = Constants.WebQueueName; 20 | 21 | this.connection = new ConnectionFactory 22 | { 23 | HostName = HostName 24 | }.CreateConnection(); 25 | 26 | this.channel = this.connection.CreateModel().AsActivityEnabled(HostName); 27 | channel.QueueDeclare(queue: Constants.FirstQueueName, exclusive: false); 28 | } 29 | 30 | public void Publish(string message) 31 | { 32 | channel.BasicPublish("", QueueName, null, System.Text.Encoding.UTF8.GetBytes(message)); 33 | } 34 | 35 | public void Dispose() 36 | { 37 | this.channel?.Close(); 38 | this.connection?.Close(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Sample.MainApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 0.2 6 | Linux 7 | ..\.. 8 | ..\..\docker-compose.dcproj 9 | 8.0 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Sample.MainApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | using Microsoft.ApplicationInsights.Extensibility; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Sample.Common; 9 | using Sample.MainApi.HostedServices; 10 | 11 | namespace Sample.MainApi 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddControllers(); 26 | services.AddHttpClient(); 27 | services.AddHostedService(); 28 | services.AddSampleAppOptions(Configuration); 29 | services.AddSingleton(); 30 | services.AddSingleton(x => (Metrics)x.GetRequiredService()); 31 | services.AddSingleton(); 32 | services.AddWebSampleTelemetry(Configuration, (b) => 33 | { 34 | b.AddCollector(t => new RabbitMQCollector.OpenTelemetry.RabbitMQCollector(t)); 35 | }); 36 | 37 | var sampleAppOptions = Configuration.GetSampleAppOptions(); 38 | 39 | if (sampleAppOptions.UseApplicationInsights) 40 | { 41 | services.AddSingleton(); 42 | } 43 | 44 | // Quick way to create channel 45 | var channel = Channel.CreateBounded(2); 46 | services.AddSingleton>(channel); 47 | services.AddSingleton>(channel); 48 | } 49 | 50 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 51 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 52 | { 53 | if (env.IsDevelopment()) 54 | { 55 | app.UseDeveloperExceptionPage(); 56 | } 57 | 58 | app.UseRouting(); 59 | 60 | app.UseAuthorization(); 61 | 62 | app.UseEndpoints(endpoints => 63 | { 64 | endpoints.MapControllers(); 65 | }); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Sample.MainApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | }, 8 | "ApplicationInsights": { 9 | "LogLevel": { 10 | "Default": "Warning", 11 | "Sample": "Information" 12 | } 13 | } 14 | }, 15 | "AllowedHosts": "*", 16 | "SampleApp": { 17 | "UseApplicationInsights": false, 18 | "UseOpenTelemetry": true, 19 | "ApplicationInsightsInstrumentationKey": "", 20 | "ApplicationInsightsForOpenTelemetryInstrumentationKey": "" 21 | }, 22 | "OpenTelemetry": { 23 | "Jaeger": { 24 | "AgentHost": "localhost", 25 | "MaxPacketSize": 1000 26 | }, 27 | "Prometheus": { 28 | "Url": "http://localhost:9184/metrics/" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/ActivityEnabledModel.cs: -------------------------------------------------------------------------------- 1 | using RabbitMQ.Client; 2 | using RabbitMQ.Client.Events; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | 8 | namespace Sample.RabbitMQCollector 9 | { 10 | /// 11 | /// Wrapper for , publishing 12 | /// For the simplicity purpose only has activity publishing 13 | /// 14 | public sealed class ActivityEnabledModel : IModel 15 | { 16 | static DiagnosticSource diagnosticSource = new DiagnosticListener(Constants.DiagnosticsName); 17 | 18 | private readonly IModel model; 19 | private readonly string hostname; 20 | 21 | public ActivityEnabledModel(IModel model) 22 | { 23 | this.model = model ?? throw new ArgumentNullException(nameof(model)); 24 | } 25 | 26 | public ActivityEnabledModel(IModel model, string hostname) 27 | { 28 | if (string.IsNullOrWhiteSpace(hostname)) 29 | { 30 | throw new ArgumentException("message", nameof(hostname)); 31 | } 32 | 33 | this.model = model ?? throw new ArgumentNullException(nameof(model)); 34 | this.hostname = hostname; 35 | } 36 | 37 | public int ChannelNumber => model.ChannelNumber; 38 | 39 | public ShutdownEventArgs CloseReason => model.CloseReason; 40 | 41 | public IBasicConsumer DefaultConsumer 42 | { 43 | get => model.DefaultConsumer; 44 | set => model.DefaultConsumer = value; 45 | } 46 | 47 | public bool IsClosed => model.IsClosed; 48 | 49 | public bool IsOpen => model.IsOpen; 50 | 51 | public ulong NextPublishSeqNo => model.NextPublishSeqNo; 52 | 53 | public TimeSpan ContinuationTimeout 54 | { 55 | get => model.ContinuationTimeout; 56 | set => model.ContinuationTimeout = value; 57 | } 58 | 59 | public event EventHandler BasicAcks 60 | { 61 | add => model.BasicAcks += value; 62 | remove => model.BasicAcks -= value; 63 | } 64 | 65 | public event EventHandler BasicNacks 66 | { 67 | add => model.BasicNacks += value; 68 | remove => model.BasicNacks -= value; 69 | } 70 | 71 | public event EventHandler BasicRecoverOk 72 | { 73 | add => model.BasicRecoverOk += value; 74 | remove => model.BasicRecoverOk -= value; 75 | } 76 | 77 | public event EventHandler BasicReturn 78 | { 79 | add => model.BasicReturn += value; 80 | remove => model.BasicReturn -= value; 81 | } 82 | 83 | public event EventHandler CallbackException 84 | { 85 | add => model.CallbackException += value; 86 | remove => model.CallbackException -= value; 87 | } 88 | 89 | public event EventHandler FlowControl 90 | { 91 | add => model.FlowControl += value; 92 | remove => model.FlowControl -= value; 93 | } 94 | 95 | public event EventHandler ModelShutdown 96 | { 97 | add => model.ModelShutdown += value; 98 | remove => model.ModelShutdown -= value; 99 | } 100 | 101 | public void Abort() => model.Abort(); 102 | 103 | public void Abort(ushort replyCode, string replyText) => model.Abort(replyCode, replyText); 104 | 105 | public void BasicAck(ulong deliveryTag, bool multiple) 106 | { 107 | model.BasicAck(deliveryTag, multiple); 108 | } 109 | 110 | public void BasicCancel(string consumerTag) 111 | { 112 | model.BasicCancel(consumerTag); 113 | } 114 | 115 | public string BasicConsume(string queue, bool autoAck, string consumerTag, bool noLocal, bool exclusive, IDictionary arguments, IBasicConsumer consumer) 116 | { 117 | return model.BasicConsume(queue, autoAck, consumerTag, noLocal, exclusive, arguments, consumer); 118 | } 119 | 120 | public BasicGetResult BasicGet(string queue, bool autoAck) 121 | { 122 | return model.BasicGet(queue, autoAck); 123 | } 124 | 125 | public void BasicNack(ulong deliveryTag, bool multiple, bool requeue) 126 | { 127 | model.BasicNack(deliveryTag, multiple, requeue); 128 | } 129 | 130 | public void BasicPublish(string exchange, string routingKey, bool mandatory, IBasicProperties basicProperties, byte[] body) 131 | { 132 | Activity activity = null; 133 | if (diagnosticSource.IsEnabled(Constants.DiagnosticsName)) 134 | { 135 | activity = new Activity(Constants.PublishActivityName); 136 | activity.AddTag(Constants.OperationTagName, Constants.PublishOperation); 137 | activity.AddTag(Constants.MessageSizeTagName, (body?.Length ?? 0).ToString(CultureInfo.InvariantCulture)); 138 | if (this.hostname != null) 139 | activity.AddTag(Constants.HostTagName, this.hostname); 140 | 141 | if (!string.IsNullOrWhiteSpace(exchange)) 142 | activity.AddTag(Constants.ExchangeTagName, exchange); 143 | 144 | if (!string.IsNullOrWhiteSpace(routingKey)) 145 | activity.AddTag(Constants.RoutingKeyTagName, routingKey); 146 | 147 | diagnosticSource.StartActivity(activity, null); 148 | } 149 | 150 | // Add into the header the current activity identifier 151 | basicProperties = basicProperties ?? model.CreateBasicProperties(); 152 | if (basicProperties.Headers == null) 153 | { 154 | basicProperties.Headers = new Dictionary(); 155 | } 156 | 157 | basicProperties.Headers.Add(TraceParent.HeaderKey, Activity.Current.Id); 158 | 159 | try 160 | { 161 | model.BasicPublish(exchange, routingKey, mandatory, basicProperties, body); 162 | } 163 | finally 164 | { 165 | if (activity != null) 166 | { 167 | diagnosticSource.StopActivity(activity, null); 168 | } 169 | } 170 | } 171 | 172 | public void BasicQos(uint prefetchSize, ushort prefetchCount, bool global) 173 | { 174 | model.BasicQos(prefetchSize, prefetchCount, global); 175 | } 176 | 177 | public void BasicRecover(bool requeue) 178 | { 179 | model.BasicRecover(requeue); 180 | } 181 | 182 | public void BasicRecoverAsync(bool requeue) 183 | { 184 | model.BasicRecoverAsync(requeue); 185 | } 186 | 187 | public void BasicReject(ulong deliveryTag, bool requeue) 188 | { 189 | model.BasicReject(deliveryTag, requeue); 190 | } 191 | 192 | public void Close() 193 | { 194 | model.Close(); 195 | } 196 | 197 | public void Close(ushort replyCode, string replyText) 198 | { 199 | model.Close(replyCode, replyText); 200 | } 201 | 202 | public void ConfirmSelect() 203 | { 204 | model.ConfirmSelect(); 205 | } 206 | 207 | public uint ConsumerCount(string queue) 208 | { 209 | return model.ConsumerCount(queue); 210 | } 211 | 212 | public IBasicProperties CreateBasicProperties() 213 | { 214 | return model.CreateBasicProperties(); 215 | } 216 | 217 | public IBasicPublishBatch CreateBasicPublishBatch() 218 | { 219 | return model.CreateBasicPublishBatch(); 220 | } 221 | 222 | public void Dispose() 223 | { 224 | model.Dispose(); 225 | } 226 | 227 | public void ExchangeBind(string destination, string source, string routingKey, IDictionary arguments) 228 | { 229 | model.ExchangeBind(destination, source, routingKey, arguments); 230 | } 231 | 232 | public void ExchangeBindNoWait(string destination, string source, string routingKey, IDictionary arguments) 233 | { 234 | model.ExchangeBindNoWait(destination, source, routingKey, arguments); 235 | } 236 | 237 | public void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments) 238 | { 239 | model.ExchangeDeclare(exchange, type, durable, autoDelete, arguments); 240 | } 241 | 242 | public void ExchangeDeclareNoWait(string exchange, string type, bool durable, bool autoDelete, IDictionary arguments) 243 | { 244 | model.ExchangeDeclareNoWait(exchange, type, durable, autoDelete, arguments); 245 | } 246 | 247 | public void ExchangeDeclarePassive(string exchange) 248 | { 249 | model.ExchangeDeclarePassive(exchange); 250 | } 251 | 252 | public void ExchangeDelete(string exchange, bool ifUnused) 253 | { 254 | model.ExchangeDelete(exchange, ifUnused); 255 | } 256 | 257 | public void ExchangeDeleteNoWait(string exchange, bool ifUnused) 258 | { 259 | model.ExchangeDeleteNoWait(exchange, ifUnused); 260 | } 261 | 262 | public void ExchangeUnbind(string destination, string source, string routingKey, IDictionary arguments) 263 | { 264 | model.ExchangeUnbind(destination, source, routingKey, arguments); 265 | } 266 | 267 | public void ExchangeUnbindNoWait(string destination, string source, string routingKey, IDictionary arguments) 268 | { 269 | model.ExchangeUnbindNoWait(destination, source, routingKey, arguments); 270 | } 271 | 272 | public uint MessageCount(string queue) 273 | { 274 | return model.MessageCount(queue); 275 | } 276 | 277 | public void QueueBind(string queue, string exchange, string routingKey, IDictionary arguments) 278 | { 279 | model.QueueBind(queue, exchange, routingKey, arguments); 280 | } 281 | 282 | public void QueueBindNoWait(string queue, string exchange, string routingKey, IDictionary arguments) 283 | { 284 | model.QueueBindNoWait(queue, exchange, routingKey, arguments); 285 | } 286 | 287 | public QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) 288 | { 289 | return model.QueueDeclare(queue, durable, exclusive, autoDelete, arguments); 290 | } 291 | 292 | public void QueueDeclareNoWait(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary arguments) 293 | { 294 | model.QueueDeclareNoWait(queue, durable, exclusive, autoDelete, arguments); 295 | } 296 | 297 | public QueueDeclareOk QueueDeclarePassive(string queue) 298 | { 299 | return model.QueueDeclarePassive(queue); 300 | } 301 | 302 | public uint QueueDelete(string queue, bool ifUnused, bool ifEmpty) 303 | { 304 | return model.QueueDelete(queue, ifUnused, ifEmpty); 305 | } 306 | 307 | public void QueueDeleteNoWait(string queue, bool ifUnused, bool ifEmpty) 308 | { 309 | model.QueueDeleteNoWait(queue, ifUnused, ifEmpty); 310 | } 311 | 312 | public uint QueuePurge(string queue) 313 | { 314 | return model.QueuePurge(queue); 315 | } 316 | 317 | public void QueueUnbind(string queue, string exchange, string routingKey, IDictionary arguments) 318 | { 319 | model.QueueUnbind(queue, exchange, routingKey, arguments); 320 | } 321 | 322 | public void TxCommit() 323 | { 324 | model.TxCommit(); 325 | } 326 | 327 | public void TxRollback() 328 | { 329 | model.TxRollback(); 330 | } 331 | 332 | public void TxSelect() 333 | { 334 | model.TxSelect(); 335 | } 336 | 337 | public bool WaitForConfirms() 338 | { 339 | return model.WaitForConfirms(); 340 | } 341 | 342 | public bool WaitForConfirms(TimeSpan timeout) 343 | { 344 | return model.WaitForConfirms(timeout); 345 | } 346 | 347 | public bool WaitForConfirms(TimeSpan timeout, out bool timedOut) 348 | { 349 | return WaitForConfirms(timeout, out timedOut); 350 | } 351 | 352 | public void WaitForConfirmsOrDie() 353 | { 354 | model.WaitForConfirmsOrDie(); 355 | } 356 | 357 | public void WaitForConfirmsOrDie(TimeSpan timeout) 358 | { 359 | model.WaitForConfirmsOrDie(timeout); 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/ActivityExtensions.cs: -------------------------------------------------------------------------------- 1 | using RabbitMQ.Client.Events; 2 | using System.Diagnostics; 3 | using System.Text; 4 | 5 | namespace Sample.RabbitMQCollector 6 | { 7 | public static class ActivityExtensions 8 | { 9 | /// 10 | /// Extracts activity from RabbitMQ message 11 | /// 12 | /// 13 | /// 14 | /// 15 | public static Activity ExtractActivity(this BasicDeliverEventArgs source, string name) 16 | { 17 | var activity = new Activity(name ?? Constants.RabbitMQMessageActivityName); 18 | 19 | if (source.BasicProperties.Headers.TryGetValue(TraceParent.HeaderKey, out var rawTraceParent) && rawTraceParent is byte[] binRawTraceParent) 20 | { 21 | activity.SetParentId(Encoding.UTF8.GetString(binRawTraceParent)); 22 | } 23 | 24 | return activity; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/ApplicationInsights/DiagnosticSourceListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Sample.RabbitMQCollector.ApplicationInsights 6 | { 7 | internal class DiagnosticSourceListener : IObserver> 8 | { 9 | public DiagnosticSourceListener() 10 | { 11 | } 12 | 13 | public void OnCompleted() 14 | { 15 | } 16 | 17 | public void OnError(Exception error) 18 | { 19 | } 20 | 21 | public void OnNext(KeyValuePair value) 22 | { 23 | if (Activity.Current == null) 24 | { 25 | //CollectorEventSource.Log.NullActivity(value.Key); 26 | return; 27 | } 28 | 29 | try 30 | { 31 | if (value.Key.EndsWith("Start")) 32 | { 33 | OnStartActivity(Activity.Current, value.Value); 34 | } 35 | else if (value.Key.EndsWith("Stop")) 36 | { 37 | this.OnStopActivity(Activity.Current, value.Value); 38 | } 39 | else if (value.Key.EndsWith("Exception")) 40 | { 41 | this.OnException(Activity.Current, value.Value); 42 | } 43 | else 44 | { 45 | this.OnCustom(value.Key, Activity.Current, value.Value); 46 | } 47 | } 48 | catch (Exception) 49 | { 50 | //CollectorEventSource.Log.UnknownErrorProcessingEvent(this.handler?.SourceName, value.Key, ex); 51 | } 52 | } 53 | 54 | protected virtual void OnCustom(string key, Activity current, object value) 55 | { 56 | } 57 | 58 | protected virtual void OnException(Activity current, object value) 59 | { 60 | } 61 | 62 | protected virtual void OnStopActivity(Activity current, object value) 63 | { 64 | } 65 | 66 | protected virtual void OnStartActivity(Activity current, object value) 67 | { 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/ApplicationInsights/RabbitMQApplicationInsightsModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.ApplicationInsights; 3 | using Microsoft.ApplicationInsights.Extensibility; 4 | 5 | namespace Sample.RabbitMQCollector.ApplicationInsights 6 | { 7 | public class RabbitMQApplicationInsightsModule : ITelemetryModule, IDisposable 8 | { 9 | private RabbitMQCollector collector; 10 | public RabbitMQApplicationInsightsModule() 11 | { 12 | 13 | } 14 | 15 | public void Initialize(TelemetryConfiguration configuration) 16 | { 17 | if (collector != null) 18 | return; 19 | 20 | collector = new RabbitMQCollector(new TelemetryClient(configuration)); 21 | collector.Subscribe(); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | collector?.Dispose(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/ApplicationInsights/RabbitMQCollector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using Microsoft.ApplicationInsights; 6 | 7 | namespace Sample.RabbitMQCollector.ApplicationInsights 8 | { 9 | public class RabbitMQCollector : IObserver 10 | { 11 | private readonly TelemetryClient client; 12 | private long disposed; 13 | private List listenerSubscriptions; 14 | private IDisposable allSourcesSubscription; 15 | 16 | 17 | public RabbitMQCollector(TelemetryClient client) 18 | { 19 | this.client = client; 20 | this.listenerSubscriptions = new List(); 21 | } 22 | 23 | public void Subscribe() 24 | { 25 | if (this.allSourcesSubscription == null) 26 | { 27 | this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); 28 | } 29 | } 30 | 31 | public void OnCompleted() 32 | { 33 | } 34 | 35 | public void OnError(Exception error) 36 | { 37 | } 38 | 39 | public void OnNext(DiagnosticListener value) 40 | { 41 | if ((Interlocked.Read(ref this.disposed) == 0)) 42 | { 43 | if (value.Name == "Sample.RabbitMQ") 44 | { 45 | var listener = new RabbitMQSourceListener(client); 46 | var subscription = value.Subscribe(listener); 47 | 48 | lock (this.listenerSubscriptions) 49 | { 50 | this.listenerSubscriptions.Add(subscription); 51 | } 52 | } 53 | } 54 | } 55 | 56 | public void Dispose() 57 | { 58 | if (Interlocked.Exchange(ref this.disposed, 1) == 1) 59 | { 60 | // already disposed 61 | return; 62 | } 63 | 64 | lock (this.listenerSubscriptions) 65 | { 66 | foreach (var listenerSubscription in this.listenerSubscriptions) 67 | { 68 | listenerSubscription?.Dispose(); 69 | } 70 | 71 | this.listenerSubscriptions.Clear(); 72 | } 73 | 74 | this.allSourcesSubscription?.Dispose(); 75 | this.allSourcesSubscription = null; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/ApplicationInsights/RabbitMQSourceListener.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.ApplicationInsights; 3 | using Microsoft.ApplicationInsights.DataContracts; 4 | 5 | namespace Sample.RabbitMQCollector.ApplicationInsights 6 | { 7 | internal class RabbitMQSourceListener : DiagnosticSourceListener 8 | { 9 | private readonly TelemetryClient client; 10 | 11 | public RabbitMQSourceListener(TelemetryClient client) 12 | { 13 | this.client = client; 14 | } 15 | 16 | protected override void OnStopActivity(Activity current, object value) 17 | { 18 | using var dependency = client.StartOperation(current); 19 | dependency.Telemetry.Type = Constants.ApplicationInsightsTelemetryType; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.RabbitMQCollector 2 | { 3 | internal class Constants 4 | { 5 | public const string DiagnosticsName = "Sample.RabbitMQ"; 6 | 7 | public const string ExchangeTagName = "exchange"; 8 | 9 | public const string RoutingKeyTagName = "routingKey"; 10 | 11 | public const string ApplicationInsightsTelemetryType = "rabbitmq"; 12 | 13 | public const string OperationTagName = "operation"; 14 | 15 | public const string MessageSizeTagName = "messageSize"; 16 | 17 | public const string PublishOperation = "publish"; 18 | 19 | public const string PublishActivityName = "Publish to RabbitMQ"; 20 | 21 | public const string HostTagName = "host"; 22 | 23 | public const string RabbitMQMessageActivityName = "RabbitMQ Message"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/IModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using Sample.RabbitMQCollector; 2 | 3 | namespace RabbitMQ.Client 4 | { 5 | public static class IModelExtensions 6 | { 7 | public static IModel AsActivityEnabled(this IModel model, string hostname) 8 | { 9 | if (model == null) 10 | return null; 11 | 12 | if (string.IsNullOrWhiteSpace(hostname)) 13 | throw new System.ArgumentException("Missing hostname", nameof(hostname)); 14 | 15 | return new ActivityEnabledModel(model, hostname); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/OpenTelemetry/RabbitMQCollector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTelemetry.Collector; 3 | using OpenTelemetry.Trace; 4 | 5 | namespace Sample.RabbitMQCollector.OpenTelemetry 6 | { 7 | 8 | public class RabbitMQCollector : IDisposable 9 | { 10 | private readonly Tracer tracer; 11 | private readonly DiagnosticSourceSubscriber subscriber; 12 | 13 | 14 | private static bool DefaultFilter(string activityName, object arg1, object unused) 15 | { 16 | return true; 17 | } 18 | 19 | public void Dispose() 20 | { 21 | this.subscriber?.Dispose(); 22 | } 23 | 24 | public RabbitMQCollector(Tracer tracer) 25 | { 26 | this.tracer = tracer; 27 | this.subscriber = new DiagnosticSourceSubscriber(new RabbitMQListener(Constants.DiagnosticsName, tracer), DefaultFilter); 28 | this.subscriber.Subscribe(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/OpenTelemetry/RabbitMQListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using OpenTelemetry.Collector; 4 | using OpenTelemetry.Trace; 5 | 6 | namespace Sample.RabbitMQCollector.OpenTelemetry 7 | { 8 | public class RabbitMQListener : ListenerHandler 9 | { 10 | public RabbitMQListener(string sourceName, Tracer tracer) : base(sourceName, tracer) 11 | { 12 | } 13 | 14 | public override void OnStartActivity(Activity activity, object payload) 15 | { 16 | var span = this.Tracer.StartSpanFromActivity(activity.OperationName, activity); 17 | foreach (var kv in activity.Tags) 18 | { 19 | span.SetAttribute(kv.Key, kv.Value); 20 | } 21 | } 22 | 23 | public override void OnStopActivity(Activity activity, object payload) 24 | { 25 | var span = this.Tracer.CurrentSpan; 26 | span.End(); 27 | if (span is IDisposable disposableSpan) 28 | { 29 | disposableSpan.Dispose(); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/Sample.RabbitMQCollector.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQCollector/TraceParent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace Sample.RabbitMQCollector 5 | { 6 | public class TraceParent 7 | { 8 | const string DefaultVersion = "00"; 9 | public const string HeaderKey = "traceparent"; 10 | 11 | public ActivityTraceId TraceId { get; } 12 | public ActivitySpanId SpanId { get; } 13 | public ActivityTraceFlags Flags { get; } 14 | public string Version { get; } 15 | public ActivityTraceFlags TraceFlags { get; } 16 | 17 | public TraceParent(ActivityTraceId traceId, ActivitySpanId spanId, ActivityTraceFlags flags = ActivityTraceFlags.None, string version = DefaultVersion) 18 | { 19 | if (string.IsNullOrWhiteSpace(version)) 20 | { 21 | throw new ArgumentException("message", nameof(version)); 22 | } 23 | 24 | TraceId = traceId; 25 | SpanId = spanId; 26 | Flags = flags; 27 | Version = version; 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return string.Join("-", new[] { Version, TraceId.ToString(), SpanId.ToString(), ((int)Flags).ToString("00") }); 33 | } 34 | 35 | public static TraceParent FromCurrentActivity() 36 | { 37 | var activity = Activity.Current; 38 | if (activity == null) 39 | throw new InvalidOperationException("No current activity"); 40 | 41 | return FromActivity(activity); 42 | } 43 | 44 | public static TraceParent FromActivity(Activity activity) 45 | { 46 | if (activity is null) 47 | { 48 | throw new ArgumentNullException(nameof(activity)); 49 | } 50 | 51 | return new TraceParent(activity.TraceId, activity.SpanId, activity.ActivityTraceFlags, DefaultVersion); 52 | } 53 | 54 | public static TraceParent CreateFromString(string traceparent) 55 | { 56 | if (string.IsNullOrWhiteSpace(traceparent)) 57 | { 58 | throw new ArgumentException("Invalid traceparent", nameof(traceparent)); 59 | } 60 | 61 | var vals = traceparent.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); 62 | if (vals.Length != 4) 63 | { 64 | throw new ArgumentException("Invalid traceparent format: {traceparent}", traceparent); 65 | } 66 | 67 | var traceId = ActivityTraceId.CreateFromString(vals[1].AsSpan()); 68 | var spanId = ActivitySpanId.CreateFromString(vals[2].AsSpan()); 69 | var flags = vals[3] == "01" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None; 70 | 71 | // TODO: validate each item 72 | return new TraceParent(traceId, spanId, flags, vals[0]); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base 4 | WORKDIR /app 5 | 6 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 7 | WORKDIR /src 8 | COPY ["src/Sample.Common/Sample.Common.csproj", "src/Sample.Common/"] 9 | COPY ["src/Sample.RabbitMQCollector/Sample.RabbitMQCollector.csproj", "src/Sample.RabbitMQCollector/"] 10 | COPY ["NuGet.config", "./"] 11 | COPY ["src/Sample.RabbitMQProcessor/Sample.RabbitMQProcessor.csproj", "src/Sample.RabbitMQProcessor/"] 12 | RUN dotnet restore "src/Sample.RabbitMQProcessor/Sample.RabbitMQProcessor.csproj" 13 | COPY . . 14 | WORKDIR "/src/src/Sample.RabbitMQProcessor" 15 | RUN dotnet build "Sample.RabbitMQProcessor.csproj" -c Release -o /app/build 16 | 17 | FROM build AS publish 18 | RUN dotnet publish "Sample.RabbitMQProcessor.csproj" -c Release -o /app/publish 19 | 20 | FROM base AS final 21 | WORKDIR /app 22 | COPY --from=publish /app/publish . 23 | ENTRYPOINT ["dotnet", "Sample.RabbitMQProcessor.dll"] -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/InvalidEventNameException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Sample.RabbitMQProcessor 5 | { 6 | [Serializable] 7 | public class InvalidEventNameException : Exception 8 | { 9 | public InvalidEventNameException() 10 | { 11 | } 12 | 13 | public InvalidEventNameException(string message) : base(message) 14 | { 15 | } 16 | 17 | public InvalidEventNameException(string message, Exception innerException) : base(message, innerException) 18 | { 19 | } 20 | 21 | protected InvalidEventNameException(SerializationInfo info, StreamingContext context) : base(info, context) 22 | { 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/Metrics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.ApplicationInsights; 4 | using Microsoft.ApplicationInsights.Metrics; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using OpenTelemetry.Metrics; 7 | using OpenTelemetry.Metrics.Configuration; 8 | using OpenTelemetry.Trace; 9 | using Sample.Common; 10 | 11 | namespace Sample.RabbitMQProcessor 12 | { 13 | public class Metrics : IAppMetrics 14 | { 15 | private readonly Metric appInsightsProcessedItemCounter; 16 | private readonly Metric appInsightsProcessedFailedItemCounter; 17 | 18 | private Meter meter; 19 | private Counter openTelemetryProcessedItemCounter; 20 | private Counter openTelemetryProcessedFailedItemCounter; 21 | 22 | public Metrics(IServiceProvider serviceProvider) 23 | { 24 | var telemetryClient = serviceProvider.GetService(); 25 | if (telemetryClient != null) 26 | { 27 | this.appInsightsProcessedItemCounter = telemetryClient.GetMetric(new MetricIdentifier("Sample App", "Processed Item", "Source")); 28 | this.appInsightsProcessedFailedItemCounter = telemetryClient.GetMetric(new MetricIdentifier("Sample App", "Processed Failed Item", "Source")); 29 | } 30 | } 31 | 32 | void IAppMetrics.Initialize(MeterFactory meterFactory) 33 | { 34 | this.meter = meterFactory.GetMeter("Sample App"); 35 | this.openTelemetryProcessedItemCounter = meter.CreateInt64Counter("Processed Item"); 36 | this.openTelemetryProcessedFailedItemCounter = meter.CreateInt64Counter("Processed Failed Item"); 37 | 38 | } 39 | 40 | public void TrackItemProcessed(double metricValue, string source, bool succeeded) 41 | { 42 | appInsightsProcessedItemCounter?.TrackValue(succeeded ? 1 : 0, source); 43 | appInsightsProcessedFailedItemCounter?.TrackValue(succeeded ? 0 : 1, source); 44 | 45 | if (meter != null) 46 | { 47 | var context = default(SpanContext); 48 | var labelSet = new Dictionary() 49 | { 50 | { "Source", source }, 51 | }; 52 | 53 | openTelemetryProcessedItemCounter.Add(context, succeeded ? 1 : 0, this.meter.GetLabelSet(labelSet)); 54 | openTelemetryProcessedFailedItemCounter.Add(context, succeeded ? 0 : 1, this.meter.GetLabelSet(labelSet)); 55 | 56 | // Collect is called here explicitly as there is 57 | // no controller implementation yet. 58 | // TODO: There should be no need to cast to MeterSdk. 59 | //(meter as MeterSdk).Collect(); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Sample.Common; 5 | 6 | namespace Sample.RabbitMQProcessor 7 | { 8 | class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | Activity.DefaultIdFormat = ActivityIdFormat.W3C; 13 | Activity.ForceDefaultIdFormat = true; 14 | CreateHostBuilder(args).Build().Run(); 15 | } 16 | 17 | public static IHostBuilder CreateHostBuilder(string[] args) 18 | { 19 | return Host.CreateDefaultBuilder(args) 20 | #if DEBUG 21 | .UseEnvironment("Development") 22 | #endif 23 | .ConfigureServices((hostContext, services) => 24 | { 25 | services.AddWorkerSampleTelemetry(hostContext.Configuration); 26 | services.AddSingleton(); 27 | services.AddSingleton(x => (Metrics)x.GetRequiredService()); 28 | 29 | services.AddHttpClient(); 30 | services.AddHostedService(); 31 | services.AddSampleAppOptions(hostContext.Configuration); 32 | }) 33 | .ConfigureLogging(SampleServiceCollectionExtensions.ConfigureLogging) 34 | .ConfigureAppConfiguration((builder) => SampleServiceCollectionExtensions.ConfigureAppConfiguration(builder, args, typeof(Program).Assembly)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Sample.RabbitMQProcessor": { 4 | "commandName": "Project" 5 | }, 6 | "Docker": { 7 | "commandName": "Docker" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/Sample.RabbitMQProcessor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 0.2 7 | 8.0 8 | Linux 9 | ..\.. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/WebQueueConsumerHostedService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using OpenTelemetry.Trace; 8 | using RabbitMQ.Client; 9 | using RabbitMQ.Client.Events; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Sample.Common; 12 | using Microsoft.ApplicationInsights; 13 | using Microsoft.ApplicationInsights.DataContracts; 14 | using Microsoft.ApplicationInsights.Extensibility; 15 | using System.Text.Json; 16 | using System.Text; 17 | using Microsoft.Extensions.Options; 18 | using RabbitMQ.Client.Exceptions; 19 | using Sample.RabbitMQCollector; 20 | 21 | namespace Sample.RabbitMQProcessor 22 | { 23 | public class WebQueueConsumerHostedService : IHostedService 24 | { 25 | private string rabbitMQHostName; 26 | private IConnection connection; 27 | private IModel channel; 28 | private AsyncEventingBasicConsumer consumer; 29 | 30 | private string timeApiURL; 31 | private readonly ILogger logger; 32 | private readonly IHttpClientFactory httpClientFactory; 33 | private readonly Metrics metrics; 34 | private readonly Tracer tracer; 35 | private readonly TelemetryClient telemetryClient; 36 | private readonly JsonSerializerOptions jsonSerializerOptions; 37 | 38 | public WebQueueConsumerHostedService(IOptions sampleAppOptions, 39 | ILogger logger, 40 | IHttpClientFactory httpClientFactory, 41 | IServiceProvider serviceProvider, 42 | Metrics metrics) 43 | { 44 | // To start RabbitMQ on docker: 45 | // docker run -d --hostname -rabbit --name test-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management 46 | this.rabbitMQHostName = sampleAppOptions.Value.RabbitMQHostName; 47 | 48 | this.timeApiURL = sampleAppOptions.Value.TimeAPIUrl; 49 | this.logger = logger; 50 | this.httpClientFactory = httpClientFactory; 51 | this.metrics = metrics; 52 | 53 | // Only using Service Provider because some of the services might not have been registered 54 | // depending on the choice of the SDK 55 | var tracerFactory = serviceProvider.GetService(); 56 | this.tracer = tracerFactory?.GetApplicationTracer(); 57 | this.telemetryClient = serviceProvider.GetService(); 58 | this.jsonSerializerOptions = new JsonSerializerOptions 59 | { 60 | PropertyNameCaseInsensitive = true, 61 | }; 62 | } 63 | 64 | public async Task StartAsync(CancellationToken cancellationToken) 65 | { 66 | while (!cancellationToken.IsCancellationRequested) 67 | { 68 | try 69 | { 70 | var factory = new ConnectionFactory() { HostName = rabbitMQHostName, DispatchConsumersAsync = true }; 71 | this.connection = factory.CreateConnection(); 72 | this.channel = connection.CreateModel(); 73 | 74 | channel.QueueDeclare(queue: Constants.WebQueueName, exclusive: false); 75 | 76 | this.consumer = new AsyncEventingBasicConsumer(channel); 77 | consumer.Received += ProcessWebQueueMessageAsync; 78 | channel.BasicConsume(queue: Constants.WebQueueName, 79 | autoAck: true, 80 | consumer: consumer); 81 | 82 | logger.LogInformation("RabbitMQ consumer started, connected to {hostname}", rabbitMQHostName); 83 | return; 84 | } 85 | catch (BrokerUnreachableException ex) 86 | { 87 | logger.LogError(ex, "Failed to connect to RabbitMQ at {hostname}. Trying again in 3 seconds", rabbitMQHostName); 88 | 89 | if (this.consumer != null && this.channel != null) 90 | { 91 | this.channel.BasicCancel(this.consumer.ConsumerTag); 92 | } 93 | 94 | this.channel?.Dispose(); 95 | 96 | this.connection?.Dispose(); 97 | 98 | 99 | try 100 | { 101 | await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); 102 | } 103 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 104 | { 105 | } 106 | } 107 | } 108 | } 109 | 110 | 111 | 112 | private async Task ProcessWebQueueMessageAsync(object sender, BasicDeliverEventArgs @event) 113 | { 114 | // ExtractActivity creates the Activity setting the parent based on the RabbitMQ "traceparent" header 115 | var activity = @event.ExtractActivity("Process single RabbitMQ message"); 116 | 117 | ISpan span = null; 118 | IOperationHolder operation = null; 119 | var processingSucceeded = false; 120 | string source = string.Empty; 121 | 122 | IDisposable loggingScope = null; 123 | 124 | try 125 | { 126 | if (tracer != null) 127 | { 128 | // OpenTelemetry seems to require the Activity to have started, unlike AI SDK 129 | activity.Start(); 130 | tracer.StartActiveSpanFromActivity(activity.OperationName, activity, SpanKind.Consumer, out span); 131 | 132 | span.SetAttribute("queue", Constants.WebQueueName); 133 | } 134 | 135 | using (operation = telemetryClient?.StartOperation(activity)) 136 | { 137 | if (operation != null) 138 | { 139 | operation.Telemetry.Properties.Add("queue", Constants.WebQueueName); 140 | operation.Telemetry.Type = ApplicationInformation.Name; 141 | operation.Telemetry.Target = this.rabbitMQHostName; 142 | } 143 | 144 | loggingScope = logger.BeginScope("Starting message processing"); 145 | 146 | // Get the payload 147 | var message = JsonSerializer.Deserialize(@event.Body, jsonSerializerOptions); 148 | if (logger.IsEnabled(LogLevel.Information)) 149 | { 150 | logger.LogInformation("Processing message from {source}: {message}", message.Source, Encoding.UTF8.GetString(@event.Body)); 151 | } 152 | 153 | source = message.Source; 154 | 155 | if ("error".Equals(message.EventName, StringComparison.OrdinalIgnoreCase)) 156 | { 157 | throw new InvalidEventNameException("Invalid event name"); 158 | } 159 | 160 | var apiFullUrl = $"{timeApiURL}/api/time/dbtime"; 161 | var time = await httpClientFactory.CreateClient().GetStringAsync(apiFullUrl); 162 | 163 | if (!string.IsNullOrEmpty(message.EventName)) 164 | { 165 | span?.AddEvent(message.EventName); 166 | telemetryClient?.TrackEvent(message.EventName); 167 | } 168 | } 169 | processingSucceeded = true; 170 | } 171 | catch (Exception ex) 172 | { 173 | if (span != null) 174 | { 175 | span.SetAttribute("error", true); 176 | span.Status = Status.Internal.WithDescription(ex.ToString()); 177 | } 178 | 179 | if (operation != null) 180 | { 181 | operation.Telemetry.Success = false; 182 | operation.Telemetry.ResultCode = "500"; 183 | 184 | // Track exception, adding the connection to the current activity 185 | var exOperation = new ExceptionTelemetry(ex); 186 | exOperation.Context.Operation.Id = operation.Telemetry.Context.Operation.Id; 187 | exOperation.Context.Operation.ParentId = operation.Telemetry.Context.Operation.ParentId; 188 | telemetryClient.TrackException(exOperation); 189 | } 190 | 191 | logger.LogError(ex, "Failed to process message from {source}", source); 192 | } 193 | finally 194 | { 195 | span?.End(); 196 | metrics.TrackItemProcessed(1, source, processingSucceeded); 197 | loggingScope?.Dispose(); 198 | } 199 | } 200 | 201 | 202 | public Task StopAsync(CancellationToken cancellationToken) 203 | { 204 | this.channel.BasicCancel(this.consumer.ConsumerTag); 205 | this.channel.Close(); 206 | this.connection.Close(); 207 | 208 | return Task.CompletedTask; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Sample.RabbitMQProcessor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Sample.RabbitMQProcessor.WebQueueConsumerHostedService": "Information" 6 | }, 7 | "ApplicationInsights": { 8 | "LogLevel": { 9 | "Default": "Warning", 10 | "Sample": "Information", 11 | "Sample.RabbitMQProcessor.WebQueueConsumerHostedService": "Information" 12 | } 13 | } 14 | }, 15 | "SampleApp": { 16 | "UseApplicationInsights": false, 17 | "UseOpenTelemetry": true, 18 | "ApplicationInsightsInstrumentationKey": "", 19 | "ApplicationInsightsForOpenTelemetryInstrumentationKey": "" 20 | }, 21 | "OpenTelemetry": { 22 | "Jaeger": { 23 | "AgentHost": "localhost", 24 | "MaxPacketSize": 1000 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Controllers/TimeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OpenTelemetry.Trace; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Sample.Common; 7 | using Microsoft.Extensions.Logging; 8 | using Sample.TimeApi.Data; 9 | using System.Text; 10 | 11 | namespace Sample.TimeApi.Controllers 12 | { 13 | [ApiController] 14 | [Route("api/[controller]")] 15 | public class TimeController : ControllerBase 16 | { 17 | private readonly IDeviceRepository repository; 18 | private readonly ILogger logger; 19 | private readonly Tracer tracer; 20 | 21 | public TimeController(IDeviceRepository repository, IServiceProvider serviceProvider, ILogger logger) 22 | { 23 | this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); 24 | this.logger = logger; 25 | var tracerFactory = serviceProvider.GetService(); 26 | this.tracer = tracerFactory?.GetApplicationTracer(); 27 | } 28 | 29 | // GET: api/time/dbtime 30 | [HttpGet("dbtime")] 31 | public async Task GetDbTimeAsync() 32 | { 33 | FailGenerator.FailIfNeeded(1); 34 | 35 | if (logger.IsEnabled(LogLevel.Debug)) 36 | { 37 | LogRequestHeaders(); 38 | } 39 | 40 | var result = await repository.GetTimeFromSqlAsync(); 41 | 42 | logger.LogInformation("{operation} result is {result}", nameof(repository.GetTimeFromSqlAsync), result); 43 | 44 | return result; 45 | } 46 | 47 | private void LogRequestHeaders() 48 | { 49 | var logText = new StringBuilder(); 50 | logText.Append("Request headers: "); 51 | var first = true; 52 | foreach (var kv in Request.Headers) 53 | { 54 | if (first) 55 | { 56 | first = false; 57 | } 58 | else 59 | { 60 | logText.Append(", "); 61 | } 62 | 63 | logText.Append(kv.Key).Append('=').Append(kv.Value); 64 | } 65 | 66 | logger.LogDebug(logText.ToString()); 67 | } 68 | 69 | // GET: api/time/localday 70 | [HttpGet("localday")] 71 | public string GetLocalDay() 72 | { 73 | FailGenerator.FailIfNeeded(1); 74 | 75 | if (logger.IsEnabled(LogLevel.Debug)) 76 | { 77 | LogRequestHeaders(); 78 | } 79 | 80 | var result = DateTime.Now.DayOfWeek.ToString(); 81 | 82 | logger.LogInformation("Retrieved current day is {currentDay} at {time}", result, DateTime.UtcNow); 83 | return result; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Data/IDeviceRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Sample.TimeApi.Data 5 | { 6 | public interface IDeviceRepository 7 | { 8 | Task GetTimeFromSqlAsync(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Data/OpenTelemetryCollectingDeviceRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using OpenTelemetry.Trace; 4 | using Sample.Common; 5 | 6 | namespace Sample.TimeApi.Data 7 | { 8 | public class OpenTelemetryCollectingDeviceRepository : IDeviceRepository 9 | where TDeviceRepository : IDeviceRepository 10 | { 11 | private readonly TDeviceRepository repository; 12 | private Tracer tracer; 13 | 14 | public OpenTelemetryCollectingDeviceRepository(TDeviceRepository repository, TracerFactoryBase tracerFactory) 15 | { 16 | this.tracer = tracerFactory.GetTracer("sql"); 17 | this.repository = repository; 18 | } 19 | 20 | public async Task GetTimeFromSqlAsync() 21 | { 22 | var span = this.tracer.StartSpan(nameof(GetTimeFromSqlAsync), SpanKind.Client); 23 | try 24 | { 25 | FailGenerator.FailIfNeeded(1); 26 | 27 | return await this.repository.GetTimeFromSqlAsync(); 28 | } 29 | catch (Exception ex) 30 | { 31 | span.SetAttribute("error", true); 32 | span.Status = Status.Internal.WithDescription(ex.ToString()); 33 | throw; 34 | } 35 | finally 36 | { 37 | span.End(); 38 | } 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Data/SqlDeviceRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.SqlClient; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Sample.TimeApi.Data 8 | { 9 | 10 | /// 11 | /// Sql device repository 12 | /// 13 | /// 14 | /// To get started Sql Server on docker is a good option: 15 | /// docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@Word1" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 16 | /// 17 | public class SqlDeviceRepository : IDeviceRepository 18 | { 19 | private readonly string connectionString; 20 | private readonly ILogger logger; 21 | 22 | public SqlDeviceRepository(IConfiguration configuration, ILogger logger) 23 | { 24 | this.connectionString = configuration["SqlConnectionString"]; 25 | this.logger = logger; 26 | } 27 | 28 | public async Task GetTimeFromSqlAsync() 29 | { 30 | using var conn = new SqlConnection(this.connectionString); 31 | await conn.OpenAsync(); 32 | 33 | if (logger.IsEnabled(LogLevel.Debug)) 34 | { 35 | logger.LogDebug("Getting date from Sql Server"); 36 | } 37 | 38 | using var cmd = new SqlCommand("SELECT GETDATE()", conn); 39 | var res = await cmd.ExecuteScalarAsync(); 40 | 41 | return (DateTime)res; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 8 | WORKDIR /src 9 | COPY ["src/Sample.Common/Sample.Common.csproj", "src/Sample.Common/"] 10 | COPY ["src/Sample.RabbitMQCollector/Sample.RabbitMQCollector.csproj", "src/Sample.RabbitMQCollector/"] 11 | COPY ["NuGet.config", "./"] 12 | COPY ["src/Sample.TimeApi/Sample.TimeApi.csproj", "src/Sample.TimeApi/"] 13 | RUN dotnet restore "src/Sample.TimeApi/Sample.TimeApi.csproj" 14 | COPY . . 15 | WORKDIR "/src/src/Sample.TimeApi" 16 | RUN dotnet build "Sample.TimeApi.csproj" -c Release -o /app/build 17 | 18 | FROM build AS publish 19 | RUN dotnet publish "Sample.TimeApi.csproj" -c Release -o /app/publish 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app/publish . 24 | ENTRYPOINT ["dotnet", "Sample.TimeApi.dll"] -------------------------------------------------------------------------------- /src/Sample.TimeApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Sample.Common; 5 | 6 | namespace Sample.TimeApi 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | Activity.DefaultIdFormat = ActivityIdFormat.W3C; 13 | Activity.ForceDefaultIdFormat = true; 14 | CreateHostBuilder(args).Build().Run(); 15 | } 16 | 17 | public static IHostBuilder CreateHostBuilder(string[] args) => 18 | Host.CreateDefaultBuilder(args) 19 | .ConfigureWebHostDefaults(webBuilder => 20 | { 21 | webBuilder.UseStartup(); 22 | }) 23 | .ConfigureLogging(SampleServiceCollectionExtensions.ConfigureLogging) 24 | .ConfigureAppConfiguration((builder) => SampleServiceCollectionExtensions.ConfigureAppConfiguration(builder, args, typeof(Program).Assembly)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Sample.TimeApi": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "api/time/dbtime", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "applicationUrl": "http://localhost:5002" 11 | }, 12 | "Docker": { 13 | "commandName": "Docker", 14 | "launchBrowser": true, 15 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/time/dbtime", 16 | "publishAllPorts": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Sample.TimeApi/Sample.TimeApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 0.2 6 | Linux 7 | ..\.. 8 | ..\..\docker-compose.dcproj 9 | 8.0 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Sample.Common; 7 | using Sample.TimeApi.Data; 8 | 9 | namespace Sample.TimeApi 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddControllers(); 24 | services.AddSingleton(); 25 | services.AddSampleAppOptions(Configuration); 26 | services.AddWebSampleTelemetry(Configuration); 27 | 28 | var sampleAppOptions = Configuration.GetSampleAppOptions(); 29 | 30 | if (sampleAppOptions.UseOpenTelemetry) 31 | { 32 | services.AddSingleton(); 33 | services.AddSingleton>(); 34 | } 35 | else 36 | { 37 | services.AddSingleton(); 38 | } 39 | } 40 | 41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 42 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 43 | { 44 | if (env.IsDevelopment()) 45 | { 46 | app.UseDeveloperExceptionPage(); 47 | } 48 | 49 | app.UseRouting(); 50 | 51 | app.UseAuthorization(); 52 | 53 | app.UseEndpoints(endpoints => 54 | { 55 | endpoints.MapControllers(); 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Sample.TimeApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | }, 8 | "ApplicationInsights": { 9 | "LogLevel": { 10 | "Default": "Warning", 11 | "Sample": "Information", 12 | "Sample.TimeApi.Controllers.TimeController": "Information" 13 | } 14 | } 15 | }, 16 | "AllowedHosts": "*", 17 | "SqlConnectionString": "server=localhost;user id=sa;password=Pass@Word1;", 18 | "SampleApp": { 19 | "UseApplicationInsights": false, 20 | "UseOpenTelemetry": true, 21 | "ApplicationInsightsInstrumentationKey": "", 22 | "ApplicationInsightsForOpenTelemetryInstrumentationKey": "" 23 | }, 24 | "OpenTelemetry": { 25 | "Jaeger": { 26 | "AgentHost": "localhost", 27 | "MaxPacketSize": 1000 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------