├── .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 | 
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 | 
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 | 
167 |
168 | Traces with Application Insights:
169 |
170 | 
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 | 
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 |
--------------------------------------------------------------------------------