├── Images ├── Diagram.png ├── JaegerMonitor.png ├── JaegerTraceHTTP.png ├── GrafanaOTELMetrics1.png ├── GrafanaOTELMetrics2.png └── JaegerTraceRabbitMQ.png ├── Configs ├── jaeger-ui.json ├── dashboard.yml ├── datasource.yml ├── prometheus.yml ├── loki.yml ├── otel-collector-config.yml ├── logs-dashboard.json ├── grafana.ini └── dotnet-otel-dashboard.json ├── SampleDotNetOTEL.BusinessService ├── Persistence │ ├── WeatherEntry.cs │ ├── WeatherDbContext.cs │ └── WeatherDbInitializer.cs ├── Controllers │ ├── ErrorResponsePolicy.cs │ ├── WeatherForecastController.cs │ └── HelloController.cs ├── Properties │ └── launchSettings.json ├── appsettings.json ├── Dockerfile ├── Migrations │ ├── 20221227102358_Initial.cs │ ├── WeatherDbContextModelSnapshot.cs │ └── 20221227102358_Initial.Designer.cs ├── SampleDotNetOTEL.BusinessService.csproj ├── Program.cs └── Workers │ └── MessagesProcessingBackgroundService.cs ├── SampleDotNetOTEL.ProxyService ├── Properties │ └── launchSettings.json ├── Controllers │ ├── WeatherController.cs │ ├── HelloController.cs │ └── MessagesController.cs ├── appsettings.json ├── Dockerfile ├── ExternalServices │ ├── BusinessServiceClient.cs │ └── MessageBroker.cs ├── SampleDotNetOTEL.ProxyService.csproj └── Program.cs ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── SampleDotNetOTEL.sln ├── CONTRIBUTING.md ├── README.md ├── docker-compose.yml └── Diagram.xml /Images/Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/HEAD/Images/Diagram.png -------------------------------------------------------------------------------- /Images/JaegerMonitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/HEAD/Images/JaegerMonitor.png -------------------------------------------------------------------------------- /Images/JaegerTraceHTTP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/HEAD/Images/JaegerTraceHTTP.png -------------------------------------------------------------------------------- /Images/GrafanaOTELMetrics1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/HEAD/Images/GrafanaOTELMetrics1.png -------------------------------------------------------------------------------- /Images/GrafanaOTELMetrics2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/HEAD/Images/GrafanaOTELMetrics2.png -------------------------------------------------------------------------------- /Images/JaegerTraceRabbitMQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/HEAD/Images/JaegerTraceRabbitMQ.png -------------------------------------------------------------------------------- /Configs/jaeger-ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "monitor": { 3 | "menuEnabled": true 4 | }, 5 | "dependencies": { 6 | "menuEnabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Persistence/WeatherEntry.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDotNetOTEL.BusinessService.Persistence; 2 | 3 | public record WeatherEntry(DateTime Date, int TemperatureC, string? Summary); 4 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Controllers/ErrorResponsePolicy.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDotNetOTEL.BusinessService.Controllers; 2 | 3 | public class ErrorResponsePolicy(double rate) 4 | { 5 | public bool IsProduceError() 6 | { 7 | return Random.Shared.NextDouble() > rate; 8 | } 9 | } -------------------------------------------------------------------------------- /Configs/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'default' 4 | orgId: 1 5 | folder: '' 6 | folderUid: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | updateIntervalSeconds: 10 11 | options: 12 | path: /etc/grafana/provisioning/dashboards 13 | 14 | -------------------------------------------------------------------------------- /Configs/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | uid: PBFA97CFB590B2093 7 | url: http://prometheus:9090 8 | isDefault: true 9 | access: proxy 10 | editable: true 11 | 12 | - name: Loki 13 | type: loki 14 | access: proxy 15 | uid: P8E80F9AEF21F6940 16 | url: http://loki:3100 17 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "weather", 9 | "applicationUrl": "http://localhost:5150", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "launchUrl": "WeatherForecast", 9 | "applicationUrl": "http://localhost:5151", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/Controllers/WeatherController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using SampleDotNetOTEL.ProxyService.ExternalServices; 3 | 4 | namespace SampleDotNetOTEL.ProxyService.Controllers; 5 | 6 | [ApiController] 7 | [Route("[controller]")] 8 | public class WeatherController(BusinessServiceClient client) : ControllerBase 9 | { 10 | [HttpGet] 11 | public async Task Get() 12 | { 13 | return Ok(await client.GetWeatherAsync()); 14 | } 15 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a brief description of the changes introduced by this PR. 4 | 5 | ### Related Issues 6 | 7 | List any related issues here. 8 | 9 | ### Checklist 10 | 11 | - [ ] The code follows the code style of this project. 12 | - [ ] I have updated the documentation accordingly. 13 | - [ ] I have tested my changes. 14 | 15 | ### Additional Notes 16 | 17 | Any additional information or context about the pull request that would be useful to the reviewers. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # Rider 12 | .idea 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | msbuild.log 33 | msbuild.err 34 | msbuild.wrn 35 | 36 | # Visual Studio 2015 37 | .vs/ 38 | -------------------------------------------------------------------------------- /Configs/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: aggregated-trace-metrics 7 | static_configs: 8 | - targets: 9 | - otel-collector:8889 10 | 11 | - job_name: jaeger 12 | static_configs: 13 | - targets: 14 | - jaeger:14269 15 | 16 | - job_name: services 17 | static_configs: 18 | - targets: 19 | - proxy-service:80 20 | - business-service-1:80 21 | - business-service-2:80 22 | - business-service-3:80 23 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Persistence/WeatherDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace SampleDotNetOTEL.BusinessService.Persistence; 4 | 5 | public class WeatherDbContext(DbContextOptions options) 6 | : DbContext(options) 7 | { 8 | public DbSet WeatherEntries { get; set; } = null!; 9 | 10 | protected override void OnModelCreating(ModelBuilder modelBuilder) 11 | { 12 | base.OnModelCreating(modelBuilder); 13 | 14 | modelBuilder 15 | .Entity() 16 | .HasKey(t => t.Date); 17 | } 18 | } -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/Controllers/HelloController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using SampleDotNetOTEL.ProxyService.ExternalServices; 3 | 4 | namespace SampleDotNetOTEL.ProxyService.Controllers; 5 | 6 | [ApiController] 7 | [Route("[controller]")] 8 | public class HelloController(BusinessServiceClient client) : ControllerBase 9 | { 10 | [HttpGet] 11 | public async Task Get(string? name) 12 | { 13 | var response = string.IsNullOrEmpty(name) 14 | ? await client.GetHelloAsync() 15 | : await client.GetHelloAsync(name); 16 | return Ok(response); 17 | } 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using SampleDotNetOTEL.BusinessService.Persistence; 4 | 5 | namespace SampleDotNetOTEL.BusinessService.Controllers; 6 | 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class WeatherController(WeatherDbContext weatherDbContext, ErrorResponsePolicy errorResponsePolicy) : ControllerBase 10 | { 11 | [HttpGet] 12 | public async Task Get() 13 | { 14 | if (errorResponsePolicy.IsProduceError()) 15 | return StatusCode(500); 16 | return Ok(await weatherDbContext.WeatherEntries.ToListAsync()); 17 | } 18 | } -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore.Mvc": "Warning", 6 | "Microsoft.AspNetCore.Hosting": "Warning", 7 | "Microsoft.AspNetCore.Routing": "Warning", 8 | "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware": "Critical", 9 | "System.Net.Http.HttpClient": "Warning", 10 | "Microsoft.EntityFrameworkCore": "Warning" 11 | } 12 | }, 13 | "ConnectionStrings": { 14 | "RabbitMQ": "amqp://admin:password@rabbitmq:5672/sample-dotnet-otel" 15 | }, 16 | "ServiceName": "proxy-service", 17 | "BusinessServiceBaseUrl": "http://localhost:5151" 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/Controllers/MessagesController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using SampleDotNetOTEL.ProxyService.ExternalServices; 4 | 5 | namespace SampleDotNetOTEL.ProxyService.Controllers; 6 | 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class MessagesController(MessageBroker messageBroker) : ControllerBase 10 | { 11 | [HttpPost] 12 | public IActionResult Post([FromBody] MessageRequest request) 13 | { 14 | messageBroker.PublishMessage(request.Message); 15 | return Ok(); 16 | } 17 | 18 | public sealed class MessageRequest 19 | { 20 | [Required] 21 | public string Message { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore.Mvc": "Warning", 6 | "Microsoft.AspNetCore.Hosting": "Warning", 7 | "Microsoft.AspNetCore.Routing": "Warning", 8 | "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware": "Critical", 9 | "System.Net.Http.HttpClient": "Warning", 10 | "Microsoft.EntityFrameworkCore": "Warning" 11 | } 12 | }, 13 | "ConnectionStrings": { 14 | "Default": "Server=localhost;Port=5432;Database=weather;User Id=postgres;Password=admin;", 15 | "RabbitMQ": "amqp://admin:password@rabbitmq:5672/sample-dotnet-otel" 16 | }, 17 | "ServiceName": "business-service", 18 | "ErrorRate": 0.9 19 | } 20 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | 5 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 6 | WORKDIR /src 7 | COPY ["SampleDotNetOTEL.ProxyService/SampleDotNetOTEL.ProxyService.csproj", "SampleDotNetOTEL.ProxyService/"] 8 | RUN dotnet restore "SampleDotNetOTEL.ProxyService/SampleDotNetOTEL.ProxyService.csproj" 9 | COPY . . 10 | WORKDIR "/src/SampleDotNetOTEL.ProxyService" 11 | RUN dotnet build "SampleDotNetOTEL.ProxyService.csproj" -c Release -o /app/build 12 | 13 | FROM build AS publish 14 | RUN dotnet publish "SampleDotNetOTEL.ProxyService.csproj" -c Release -o /app/publish 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | COPY --from=publish /app/publish . 19 | ENTRYPOINT ["dotnet", "SampleDotNetOTEL.ProxyService.dll"] 20 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | 5 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 6 | WORKDIR /src 7 | COPY ["SampleDotNetOTEL.BusinessService/SampleDotNetOTEL.BusinessService.csproj", "SampleDotNetOTEL.BusinessService/"] 8 | RUN dotnet restore "SampleDotNetOTEL.BusinessService/SampleDotNetOTEL.BusinessService.csproj" 9 | COPY . . 10 | WORKDIR "/src/SampleDotNetOTEL.BusinessService" 11 | RUN dotnet build "SampleDotNetOTEL.BusinessService.csproj" -c Release -o /app/build 12 | 13 | FROM build AS publish 14 | RUN dotnet publish "SampleDotNetOTEL.BusinessService.csproj" -c Release -o /app/publish 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | COPY --from=publish /app/publish . 19 | ENTRYPOINT ["dotnet", "SampleDotNetOTEL.BusinessService.dll"] 20 | -------------------------------------------------------------------------------- /Configs/loki.yml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | 6 | ingester: 7 | lifecycler: 8 | address: 127.0.0.1 9 | ring: 10 | kvstore: 11 | store: inmemory 12 | replication_factor: 1 13 | final_sleep: 0s 14 | chunk_idle_period: 5m 15 | chunk_retain_period: 30s 16 | wal: 17 | enabled: false 18 | 19 | schema_config: 20 | configs: 21 | - from: 2022-01-01 22 | store: boltdb 23 | object_store: filesystem 24 | schema: v11 25 | index: 26 | prefix: index_ 27 | period: 168h 28 | 29 | storage_config: 30 | boltdb: 31 | directory: /tmp/loki/index 32 | 33 | filesystem: 34 | directory: /tmp/loki/chunks 35 | 36 | limits_config: 37 | enforce_metric_name: false 38 | reject_old_samples: true 39 | reject_old_samples_max_age: 168h 40 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Controllers/HelloController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace SampleDotNetOTEL.BusinessService.Controllers; 5 | 6 | [ApiController] 7 | [Route("[controller]")] 8 | public class HelloController(ErrorResponsePolicy errorResponsePolicy) : ControllerBase 9 | { 10 | [HttpGet] 11 | public IActionResult Get() 12 | { 13 | if (errorResponsePolicy.IsProduceError()) 14 | return StatusCode(500); 15 | return Ok("Hello World"); 16 | } 17 | 18 | [HttpPost] 19 | public IActionResult Post([FromBody] HelloRequest request) 20 | { 21 | if (errorResponsePolicy.IsProduceError()) 22 | return StatusCode(500); 23 | return Ok($"Hello {request.Name}"); 24 | } 25 | 26 | public sealed class HelloRequest 27 | { 28 | [Required] 29 | [MinLength(2)] 30 | public string? Name { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Configs/otel-collector-config.yml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | http: 6 | 7 | exporters: 8 | prometheus: 9 | endpoint: "0.0.0.0:8889" 10 | 11 | otlp: 12 | endpoint: "jaeger:4317" 13 | tls: 14 | insecure: true 15 | 16 | loki: 17 | endpoint: http://loki:3100/loki/api/v1/push 18 | 19 | connectors: 20 | spanmetrics: 21 | histogram: 22 | explicit: 23 | buckets: [ 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1000ms ] 24 | 25 | processors: 26 | resource: 27 | attributes: 28 | - action: insert 29 | key: service_name 30 | from_attribute: service.name 31 | - action: insert 32 | key: loki.resource.labels 33 | value: service_name 34 | 35 | service: 36 | pipelines: 37 | traces: 38 | receivers: [ otlp ] 39 | exporters: [ otlp, spanmetrics ] 40 | metrics: 41 | receivers: [ spanmetrics ] 42 | exporters: [ prometheus ] 43 | logs: 44 | receivers: [ otlp ] 45 | processors: [ resource ] 46 | exporters: [ loki ] 47 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/ExternalServices/BusinessServiceClient.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDotNetOTEL.ProxyService.ExternalServices; 2 | 3 | public sealed class BusinessServiceClient(HttpClient httpClient) 4 | { 5 | public async Task GetWeatherAsync() 6 | { 7 | var response = await httpClient.GetAsync("weather"); 8 | response.EnsureSuccessStatusCode(); 9 | 10 | var content = await response.Content.ReadAsStringAsync(); 11 | return content; 12 | } 13 | 14 | public async Task GetHelloAsync() 15 | { 16 | var response = await httpClient.GetAsync("hello"); 17 | response.EnsureSuccessStatusCode(); 18 | 19 | var content = await response.Content.ReadAsStringAsync(); 20 | return content; 21 | } 22 | 23 | public async Task GetHelloAsync(string name) 24 | { 25 | var response = await httpClient.PostAsJsonAsync("hello", new { Name = name }); 26 | response.EnsureSuccessStatusCode(); 27 | 28 | var content = await response.Content.ReadAsStringAsync(); 29 | return content; 30 | } 31 | } -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Migrations/20221227102358_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace SampleDotNetOTEL.BusinessService.Migrations 7 | { 8 | /// 9 | public partial class Initial : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "WeatherEntries", 16 | columns: table => new 17 | { 18 | Date = table.Column(type: "timestamp with time zone", nullable: false), 19 | TemperatureC = table.Column(type: "integer", nullable: false), 20 | Summary = table.Column(type: "text", nullable: true) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_WeatherEntries", x => x.Date); 25 | }); 26 | } 27 | 28 | /// 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropTable( 32 | name: "WeatherEntries"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/SampleDotNetOTEL.ProxyService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Linux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | .dockerignore 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Persistence/WeatherDbInitializer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Npgsql; 3 | using Polly; 4 | 5 | namespace SampleDotNetOTEL.BusinessService.Persistence; 6 | 7 | public sealed class WeatherDbInitializer(WeatherDbContext dbContext) 8 | { 9 | private static readonly string[] Summaries = 10 | { 11 | "Freezing", 12 | "Bracing", 13 | "Chilly", 14 | "Cool", 15 | "Mild", 16 | "Warm", 17 | "Balmy", 18 | "Hot", 19 | "Sweltering", 20 | "Scorching" 21 | }; 22 | 23 | public async Task InitAsync() 24 | { 25 | await Policy 26 | .Handle() 27 | .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(1)) 28 | .ExecuteAsync(async () => 29 | { 30 | await dbContext.Database.MigrateAsync(); 31 | 32 | if (!await dbContext.WeatherEntries.AnyAsync()) 33 | { 34 | dbContext.WeatherEntries 35 | .AddRange(Enumerable.Range(1, 5) 36 | .Select(index => new WeatherEntry( 37 | DateTime.UtcNow.AddDays(index), 38 | Random.Shared.Next(-20, 55), 39 | Summaries[Random.Shared.Next(Summaries.Length)]))); 40 | await dbContext.SaveChangesAsync(); 41 | } 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Migrations/WeatherDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 7 | using SampleDotNetOTEL.BusinessService.Persistence; 8 | 9 | #nullable disable 10 | 11 | namespace SampleDotNetOTEL.BusinessService.Migrations 12 | { 13 | [DbContext(typeof(WeatherDbContext))] 14 | partial class WeatherDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "7.0.1") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 22 | 23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 24 | 25 | modelBuilder.Entity("SampleDotNetOTEL.BusinessService.Persistence.WeatherEntry", b => 26 | { 27 | b.Property("Date") 28 | .HasColumnType("timestamp with time zone"); 29 | 30 | b.Property("Summary") 31 | .HasColumnType("text"); 32 | 33 | b.Property("TemperatureC") 34 | .HasColumnType("integer"); 35 | 36 | b.HasKey("Date"); 37 | 38 | b.ToTable("WeatherEntries"); 39 | }); 40 | #pragma warning restore 612, 618 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Migrations/20221227102358_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | using SampleDotNetOTEL.BusinessService.Persistence; 9 | 10 | #nullable disable 11 | 12 | namespace SampleDotNetOTEL.BusinessService.Migrations 13 | { 14 | [DbContext(typeof(WeatherDbContext))] 15 | [Migration("20221227102358_Initial")] 16 | partial class Initial 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "7.0.1") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 25 | 26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("SampleDotNetOTEL.BusinessService.Persistence.WeatherEntry", b => 29 | { 30 | b.Property("Date") 31 | .HasColumnType("timestamp with time zone"); 32 | 33 | b.Property("Summary") 34 | .HasColumnType("text"); 35 | 36 | b.Property("TemperatureC") 37 | .HasColumnType("integer"); 38 | 39 | b.HasKey("Date"); 40 | 41 | b.ToTable("WeatherEntries"); 42 | }); 43 | #pragma warning restore 612, 618 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/SampleDotNetOTEL.BusinessService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Linux 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | .dockerignore 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleDotNetOTEL.ProxyService", "SampleDotNetOTEL.ProxyService\SampleDotNetOTEL.ProxyService.csproj", "{A75B4622-09CB-45A8-9242-25693B454878}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleDotNetOTEL.BusinessService", "SampleDotNetOTEL.BusinessService\SampleDotNetOTEL.BusinessService.csproj", "{9D696FF2-BA89-4B1A-9AF0-C96DC8E2C3DE}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{2970DE25-3590-4F6D-9827-BD1FBE0C93E1}" 8 | ProjectSection(SolutionItems) = preProject 9 | docker-compose.yml = docker-compose.yml 10 | jaeger-ui.json = Configs\jaeger-ui.json 11 | prometheus.yml = Configs\prometheus.yml 12 | datasource.yml = Configs\datasource.yml 13 | grafana.ini = Configs\grafana.ini 14 | otel-collector-config.yml = Configs\otel-collector-config.yml 15 | README.md = README.md 16 | LICENSE = LICENSE 17 | Configs\loki.yml = Configs\loki.yml 18 | Configs\logs-dashboard.json = Configs\logs-dashboard.json 19 | Configs\dotnet-otel-dashboard.json = Configs\dotnet-otel-dashboard.json 20 | Configs\dashboard.yml = Configs\dashboard.yml 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {A75B4622-09CB-45A8-9242-25693B454878}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {A75B4622-09CB-45A8-9242-25693B454878}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {A75B4622-09CB-45A8-9242-25693B454878}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {A75B4622-09CB-45A8-9242-25693B454878}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {9D696FF2-BA89-4B1A-9AF0-C96DC8E2C3DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {9D696FF2-BA89-4B1A-9AF0-C96DC8E2C3DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {9D696FF2-BA89-4B1A-9AF0-C96DC8E2C3DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {9D696FF2-BA89-4B1A-9AF0-C96DC8E2C3DE}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.HttpLogging; 2 | using OpenTelemetry.Instrumentation.AspNetCore; 3 | using OpenTelemetry.Logs; 4 | using OpenTelemetry.Metrics; 5 | using OpenTelemetry.Resources; 6 | using OpenTelemetry.Trace; 7 | using SampleDotNetOTEL.ProxyService.ExternalServices; 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | 11 | builder.Logging 12 | .AddOpenTelemetry(options => 13 | { 14 | options.IncludeFormattedMessage = true; 15 | options.IncludeScopes = true; 16 | 17 | var resBuilder = ResourceBuilder.CreateDefault(); 18 | var serviceName = builder.Configuration["ServiceName"]!; 19 | resBuilder.AddService(serviceName); 20 | options.SetResourceBuilder(resBuilder); 21 | 22 | options.AddOtlpExporter(); 23 | }); 24 | 25 | builder.Services.AddControllers(); 26 | 27 | builder.Services.AddHttpLogging(o => o.LoggingFields = HttpLoggingFields.All); 28 | 29 | builder.Services.AddHttpClient(c => 30 | { 31 | var urls = builder.Configuration["BusinessServiceBaseUrl"]!.Split(';', StringSplitOptions.RemoveEmptyEntries); 32 | c.BaseAddress = new Uri(urls[Random.Shared.Next(urls.Length)]); 33 | }); 34 | 35 | builder.Services.AddSingleton(); 36 | 37 | builder.Services.Configure(options => 38 | { 39 | // Filter out instrumentation of the Prometheus scraping endpoint. 40 | options.Filter = ctx => ctx.Request.Path != "/metrics"; 41 | }); 42 | 43 | builder.Services.AddOpenTelemetry() 44 | .ConfigureResource(b => 45 | { 46 | b.AddService(builder.Configuration["ServiceName"]!); 47 | }) 48 | .WithTracing(b => b 49 | .AddAspNetCoreInstrumentation() 50 | .AddHttpClientInstrumentation() 51 | .AddSource(MessageBroker.TraceActivityName) 52 | .AddOtlpExporter()) 53 | .WithMetrics(b => b 54 | .AddAspNetCoreInstrumentation() 55 | .AddHttpClientInstrumentation() 56 | .AddRuntimeInstrumentation() 57 | .AddProcessInstrumentation() 58 | .AddPrometheusExporter()); 59 | 60 | var app = builder.Build(); 61 | 62 | app.UseOpenTelemetryPrometheusScrapingEndpoint(); 63 | app.UseHttpLogging(); 64 | app.UseDeveloperExceptionPage(); 65 | app.MapControllers(); 66 | app.Run(); -------------------------------------------------------------------------------- /SampleDotNetOTEL.ProxyService/ExternalServices/MessageBroker.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text; 3 | using RabbitMQ.Client; 4 | using RabbitMQ.Client.Exceptions; 5 | 6 | namespace SampleDotNetOTEL.ProxyService.ExternalServices; 7 | 8 | public sealed class MessageBroker : IDisposable 9 | { 10 | public static readonly string TraceActivityName = typeof(MessageBroker).FullName!; 11 | private static readonly ActivitySource TraceActivitySource = new (TraceActivityName); 12 | 13 | private readonly IConnection _connection; 14 | private readonly IModel _channel; 15 | 16 | public MessageBroker( 17 | IConfiguration configuration, 18 | IHostApplicationLifetime hostApplicationLifetime) 19 | { 20 | var factory = new ConnectionFactory 21 | { 22 | Uri = new Uri(configuration.GetConnectionString("RabbitMQ")!) 23 | }; 24 | 25 | while (!hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) 26 | { 27 | try 28 | { 29 | _connection = factory.CreateConnection(); 30 | break; 31 | } 32 | catch (BrokerUnreachableException) 33 | { 34 | // Ignore 35 | } 36 | } 37 | 38 | hostApplicationLifetime.ApplicationStopping.ThrowIfCancellationRequested(); 39 | 40 | _channel = _connection!.CreateModel(); 41 | 42 | _channel.QueueDeclare( 43 | queue: "test-messages", 44 | durable: false, 45 | exclusive: false, 46 | autoDelete: false 47 | ); 48 | } 49 | 50 | public void PublishMessage(string message) 51 | { 52 | using var activity = TraceActivitySource.StartActivity(nameof(PublishMessage), ActivityKind.Producer); 53 | 54 | var basicProperties = _channel.CreateBasicProperties(); 55 | 56 | if (activity?.Id != null) 57 | { 58 | basicProperties.Headers = new Dictionary 59 | { 60 | { "traceparent", activity.Id } 61 | }; 62 | } 63 | 64 | _channel.BasicPublish( 65 | exchange: string.Empty, 66 | routingKey: "test-messages", 67 | basicProperties: basicProperties, 68 | body: Encoding.UTF8.GetBytes(message)); 69 | } 70 | 71 | public void Dispose() 72 | { 73 | _channel.Dispose(); 74 | _connection.Dispose(); 75 | } 76 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to sample-dotnet-otel 2 | 3 | Thank you for your interest in contributing to `sample-dotnet-otel`! Your contributions play a crucial role in improving the project and advancing its integration with OpenTelemetry in the .NET ecosystem. 4 | 5 | ## Getting Started 6 | 7 | 1. **Fork the Repository**: Click the 'Fork' button at the top right of the main repository page. This will create a copy of the repository in your GitHub account. 8 | 2. **Clone the Repository**: After forking, clone the repository to your local machine using: 9 | ``` 10 | git clone https://github.com/YOUR-GITHUB-USERNAME/sample-dotnet-otel.git 11 | ``` 12 | 3. **Set Up the Project**: Follow the instructions in the README to set up the project locally. 13 | 14 | ## Contribution Guidelines 15 | 16 | ### Reporting Bugs 17 | 18 | - Ensure the bug hasn't been reported before by searching through the project's issue tracker. 19 | - If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title, clear description, and as much relevant information as possible. 20 | 21 | ### Suggesting Enhancements 22 | 23 | - Open a new issue in the repository. 24 | - Clearly describe the enhancement and the motivation for it. 25 | - Be sure to detail the current behavior and the expected behavior. 26 | 27 | ### Pull Requests 28 | 29 | 1. **Create a New Branch**: For every new feature or fix, create a new branch. 30 | ``` 31 | git checkout -b feature/your-feature-name 32 | ``` 33 | 2. **Make Your Changes**: Implement your changes, ensuring they adhere to the existing code style. 34 | 3. **Commit Your Changes**: Commit your changes with a meaningful commit message. 35 | 4. **Push to Your Fork**: Push your changes to your fork on GitHub. 36 | ``` 37 | git push origin feature/your-feature-name 38 | ``` 39 | 5. **Submit a Pull Request**: Go to the original `sample-dotnet-otel` repository and click on "New Pull Request". Choose your fork and the branch you created. Fill out the PR form and submit. 40 | 41 | ## Code Style and Standards 42 | 43 | - Ensure your code adheres to the existing style of the project. 44 | - Write clear and concise comments explaining your changes. 45 | - If you introduce new features, ensure they come with appropriate tests and documentation. 46 | 47 | ## Feedback 48 | 49 | Your feedback is valuable! If you have any comments, questions, or other feedback, feel free to open an issue in the repository. 50 | 51 | --- 52 | 53 | Thank you for your contributions and for making `sample-dotnet-otel` even better! 54 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.HttpLogging; 2 | using Microsoft.EntityFrameworkCore; 3 | using OpenTelemetry.Instrumentation.AspNetCore; 4 | using OpenTelemetry.Logs; 5 | using OpenTelemetry.Metrics; 6 | using OpenTelemetry.Resources; 7 | using OpenTelemetry.Trace; 8 | using SampleDotNetOTEL.BusinessService.Controllers; 9 | using SampleDotNetOTEL.BusinessService.Persistence; 10 | using SampleDotNetOTEL.BusinessService.Workers; 11 | 12 | var builder = WebApplication.CreateBuilder(args); 13 | 14 | builder.Logging 15 | .AddOpenTelemetry(options => 16 | { 17 | options.IncludeFormattedMessage = true; 18 | options.IncludeScopes = true; 19 | 20 | var resBuilder = ResourceBuilder.CreateDefault(); 21 | var serviceName = builder.Configuration["ServiceName"]!; 22 | resBuilder.AddService(serviceName); 23 | options.SetResourceBuilder(resBuilder); 24 | 25 | options.AddOtlpExporter(); 26 | }); 27 | 28 | builder.Services.AddControllers(); 29 | 30 | builder.Services.AddHttpLogging(o => o.LoggingFields = HttpLoggingFields.All); 31 | 32 | builder.Services.AddSingleton(new ErrorResponsePolicy(builder.Configuration.GetValue("ErrorRate"))); 33 | 34 | builder.Services.AddDbContext(b => b.UseNpgsql(builder.Configuration["ConnectionStrings:Default"])); 35 | builder.Services.AddTransient(); 36 | 37 | builder.Services.AddHostedService(); 38 | 39 | builder.Services.Configure(options => 40 | { 41 | // Filter out instrumentation of the Prometheus scraping endpoint. 42 | options.Filter = ctx => ctx.Request.Path != "/metrics"; 43 | }); 44 | 45 | builder.Services.AddOpenTelemetry() 46 | .ConfigureResource(b => 47 | { 48 | b.AddService(builder.Configuration["ServiceName"]!); 49 | }) 50 | .WithTracing(b => b 51 | .AddAspNetCoreInstrumentation() 52 | .AddHttpClientInstrumentation() 53 | .AddEntityFrameworkCoreInstrumentation() 54 | .AddSource(MessagesProcessingBackgroundService.TraceActivityName) 55 | .AddOtlpExporter()) 56 | .WithMetrics(b => b 57 | .AddAspNetCoreInstrumentation() 58 | .AddHttpClientInstrumentation() 59 | .AddRuntimeInstrumentation() 60 | .AddProcessInstrumentation() 61 | .AddPrometheusExporter()); 62 | 63 | var app = builder.Build(); 64 | 65 | using var scope = app.Services.CreateScope(); 66 | { 67 | var dbSeeder = scope.ServiceProvider.GetRequiredService(); 68 | await dbSeeder.InitAsync(); 69 | } 70 | 71 | app.UseOpenTelemetryPrometheusScrapingEndpoint(); 72 | app.UseHttpLogging(); 73 | app.UseDeveloperExceptionPage(); 74 | app.MapControllers(); 75 | app.Run(); 76 | -------------------------------------------------------------------------------- /Configs/logs-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "id": 2, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "datasource": { 33 | "type": "loki", 34 | "uid": "P8E80F9AEF21F6940" 35 | }, 36 | "gridPos": { 37 | "h": 33, 38 | "w": 24, 39 | "x": 0, 40 | "y": 0 41 | }, 42 | "id": 2, 43 | "options": { 44 | "dedupStrategy": "none", 45 | "enableLogDetails": true, 46 | "prettifyLogMessage": false, 47 | "showCommonLabels": false, 48 | "showLabels": false, 49 | "showTime": true, 50 | "sortOrder": "Descending", 51 | "wrapLogMessage": false 52 | }, 53 | "pluginVersion": "9.3.2", 54 | "targets": [ 55 | { 56 | "datasource": { 57 | "type": "loki", 58 | "uid": "P8E80F9AEF21F6940" 59 | }, 60 | "editorMode": "code", 61 | "expr": "{service_name=\"$service_name\"} | json | line_format `[{{ .level }}] {{ .body }}`", 62 | "queryType": "range", 63 | "refId": "A" 64 | } 65 | ], 66 | "title": "Logs", 67 | "type": "logs" 68 | } 69 | ], 70 | "schemaVersion": 37, 71 | "style": "dark", 72 | "tags": [], 73 | "templating": { 74 | "list": [ 75 | { 76 | "current": { 77 | "selected": false, 78 | "text": "business-service", 79 | "value": "business-service" 80 | }, 81 | "datasource": { 82 | "type": "loki", 83 | "uid": "P8E80F9AEF21F6940" 84 | }, 85 | "definition": "", 86 | "hide": 0, 87 | "includeAll": false, 88 | "label": "Service", 89 | "multi": false, 90 | "name": "service_name", 91 | "options": [], 92 | "query": { 93 | "label": "service_name", 94 | "refId": "LokiVariableQueryEditor-VariableQuery", 95 | "stream": "", 96 | "type": 1 97 | }, 98 | "refresh": 1, 99 | "regex": "", 100 | "skipUrlSync": false, 101 | "sort": 1, 102 | "type": "query" 103 | } 104 | ] 105 | }, 106 | "time": { 107 | "from": "now-5m", 108 | "to": "now" 109 | }, 110 | "timepicker": {}, 111 | "timezone": "", 112 | "title": "Logs", 113 | "uid": "g4tAIpp4z", 114 | "version": 1, 115 | "weekStart": "" 116 | } 117 | -------------------------------------------------------------------------------- /SampleDotNetOTEL.BusinessService/Workers/MessagesProcessingBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text; 3 | using RabbitMQ.Client; 4 | using RabbitMQ.Client.Events; 5 | using RabbitMQ.Client.Exceptions; 6 | 7 | namespace SampleDotNetOTEL.BusinessService.Workers; 8 | 9 | public sealed class MessagesProcessingBackgroundService : BackgroundService 10 | { 11 | public static readonly string TraceActivityName = typeof(MessagesProcessingBackgroundService).FullName!; 12 | private static readonly ActivitySource TraceActivitySource = new (TraceActivityName); 13 | 14 | private readonly ILogger _logger; 15 | 16 | private readonly IConnection _connection; 17 | private readonly IModel _channel; 18 | 19 | public MessagesProcessingBackgroundService( 20 | IConfiguration configuration, 21 | IHostApplicationLifetime hostApplicationLifetime, 22 | ILogger logger) 23 | { 24 | _logger = logger; 25 | 26 | var factory = new ConnectionFactory 27 | { 28 | Uri = new Uri(configuration.GetConnectionString("RabbitMQ")!) 29 | }; 30 | 31 | while (!hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) 32 | { 33 | try 34 | { 35 | _connection = factory.CreateConnection(); 36 | break; 37 | } 38 | catch (BrokerUnreachableException) 39 | { 40 | // Ignore 41 | } 42 | } 43 | 44 | hostApplicationLifetime.ApplicationStopping.ThrowIfCancellationRequested(); 45 | 46 | _channel = _connection!.CreateModel(); 47 | 48 | _channel.QueueDeclare( 49 | queue: "test-messages", 50 | durable: false, 51 | exclusive: false, 52 | autoDelete: false 53 | ); 54 | } 55 | 56 | protected override Task ExecuteAsync(CancellationToken stoppingToken) 57 | { 58 | var consumer = new EventingBasicConsumer(_channel); 59 | consumer.Received += (_, e) => ProcessMessage(e); 60 | 61 | _channel.BasicConsume( 62 | queue: "test-messages", 63 | autoAck: true, 64 | consumer: consumer); 65 | 66 | return Task.Delay(Timeout.Infinite, stoppingToken); 67 | } 68 | 69 | private void ProcessMessage(BasicDeliverEventArgs e) 70 | { 71 | string? parentActivityId = null; 72 | if (e.BasicProperties?.Headers?.TryGetValue("traceparent", out var parentActivityIdRaw) == true && 73 | parentActivityIdRaw is byte[] traceParentBytes) 74 | parentActivityId = Encoding.UTF8.GetString(traceParentBytes); 75 | 76 | using var activity = TraceActivitySource.StartActivity(nameof(ProcessMessage), kind: ActivityKind.Consumer, parentId: parentActivityId); 77 | 78 | var body = e.Body.ToArray(); 79 | var message = Encoding.UTF8.GetString(body); 80 | 81 | _logger.LogInformation("Received message: {Message}", message); 82 | } 83 | 84 | public override async Task StopAsync(CancellationToken stoppingToken) 85 | { 86 | await base.StopAsync(stoppingToken); 87 | 88 | _channel.Close(); 89 | _connection.Close(); 90 | } 91 | 92 | public override void Dispose() 93 | { 94 | base.Dispose(); 95 | 96 | _channel.Dispose(); 97 | _connection.Dispose(); 98 | } 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample ASP.NET (.NET 8) project with OpenTelemetry integration 2 | 3 | ## 📡 OpenTelemetry 4 | 5 | OpenTelemetry is a collection of tools, APIs, and SDKs to instrument, generate, collect, and export telemetry data (metrics, logs, and traces) to help you analyze your software’s performance and behavior. 6 | 7 | Links: 8 | * https://opentelemetry.io/ 9 | * https://opentelemetry.io/docs/ 10 | 11 | There is a .NET SDK that helps to integrate .NET applications with OpenTelemetry: 12 | * https://opentelemetry.io/docs/instrumentation/net/getting-started/ 13 | * https://github.com/open-telemetry/opentelemetry-dotnet 14 | * https://www.nuget.org/packages/OpenTelemetry/ 15 | 16 | ## 🎯 Goal 17 | 18 | The goal of this project is to play around with exporting .NET service metrics to [Prometheus](https://prometheus.io/), tracing service-to-service communication with [Jaeger](https://www.jaegertracing.io/), and collecting logs with [Loki](https://grafana.com/oss/loki/) using OpenTelementry .NET SDK. 19 | 20 | ## 📊 Components diagram 21 | 22 | ![Components Diagram](Images/Diagram.png) 23 | 24 | [Open in app.diagrams.net](https://viewer.diagrams.net/?url=https://raw.githubusercontent.com/nazarii-piontko/sample-dotnet-otel/main/Diagram.xml) 25 | 26 | ## 🔍 Implementation details 27 | 28 | Two .NET services have been implemented: 29 | 30 | The first one is `SampleDotNetOTEL.BusinessService`. It exposes 3 endpoints: 31 | * Get fake auto-generated weather records from the database (PostgreSQL). 32 | * Get `"Hello World"` string 33 | * Get `"Hello {USER_NAME}"` string for the passed username parameter. 34 | 35 | Also `SampleDotNetOTEL.BusinessService` has injected "faults" to simulate service errors. 36 | 37 | And also `SampleDotNetOTEL.BusinessService` reads messages from RabbitMQ and logs them. 38 | 39 | The second one is `SampleDotNetOTEL.ProxyService`. It exposes the same 3 endpoints as the first service but it just makes an HTTP request to `SampleDotNetOTEL.BusinessService` and forwards the response as it is. 40 | Also, it provides the fourth endpoint to `POST` message which will be queued to RabbitMQ and later read by `SampleDotNetOTEL.BusinessService` to log it. 41 | 42 | All services could be run with `docker-compose`. 43 | 44 | `docker-compose.yml` contains as one of the services a dummy client called `spammer` which makes requests to 3 endpoints every half a second. 45 | 46 | ## 🚀 How to run locally 47 | 48 | * Ensure you have `Docker` installed and running. 49 | * Ensure you have `docker-compose` installed. 50 | * Run `docker-compose build` 51 | * Run `docker-compose up` 52 | * As soon as `docker-compose` starts services they should be available via HTTP. Here are some links: 53 | * Grafana pre-build dashboard should be accessible via http://localhost:3000/d/KdDACDp4z/asp-net-otel-metrics 54 | * Jaeger should be accessible via http://localhost:16686/search 55 | * Prometheus should be accessible via http://localhost:9090/graph 56 | * Proxy service should be accessible via http://localhost:8080/hello 57 | 58 | ## 📸 Screenshots 59 | 60 | ### Jaeger 61 | 62 | #### Jaeger trace 63 | 64 | ##### Jaeger trace for HTTP 65 | ![Jaeger trace for HTTP](Images/JaegerTraceHTTP.png) 66 | 67 | ##### Jaeger trace for RabbitMQ 68 | ![Jaeger trace for RabbitMQ](Images/JaegerTraceRabbitMQ.png) 69 | 70 | #### Jaeger monitor 71 | 72 | ![Jaeger monitor](Images/JaegerMonitor.png) 73 | 74 | ### Grafana 75 | 76 | Dashboard: https://grafana.com/grafana/dashboards/17706-asp-net-otel-metrics 77 | 78 | Note: due to regular updates in OTEL metric names, ensure you download the appropriate dashboard revision. 79 | 80 | ![Grafana dashboard part 1](Images/GrafanaOTELMetrics1.png) 81 | 82 | ![Grafana dashboard part 2](Images/GrafanaOTELMetrics2.png) 83 | 84 | ## 🤝 How to Contribute 85 | 86 | Interested in contributing? Check out our [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to make this project even better! 87 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | jaeger: 5 | image: jaegertracing/all-in-one:1.52 6 | command: --query.ui-config=/etc/jaeger/jaeger-ui.json --collector.otlp.enabled=true --prometheus.server-url=http://prometheus:9090 --prometheus.query.support-spanmetrics-connector=true 7 | environment: 8 | - METRICS_STORAGE_TYPE=prometheus 9 | - PROMETHEUS_QUERY_NORMALIZE_CALLS=true 10 | - PROMETHEUS_QUERY_NORMALIZE_DURATION=true 11 | volumes: 12 | - ./Configs/jaeger-ui.json:/etc/jaeger/jaeger-ui.json 13 | ports: 14 | - "127.0.0.1:16686:16686" 15 | networks: 16 | - backend 17 | 18 | otel-collector: 19 | image: otel/opentelemetry-collector-contrib:0.91.0 20 | command: --config /etc/otelcol/otel-collector-config.yml 21 | volumes: 22 | - ./Configs/otel-collector-config.yml:/etc/otelcol/otel-collector-config.yml 23 | ports: 24 | - "127.0.0.1:4317:4317" 25 | networks: 26 | - backend 27 | depends_on: 28 | - jaeger 29 | 30 | prometheus: 31 | image: prom/prometheus:v2.48.1 32 | volumes: 33 | - ./Configs/prometheus.yml:/etc/prometheus/prometheus.yml 34 | ports: 35 | - "127.0.0.1:9090:9090" 36 | networks: 37 | - backend 38 | 39 | loki: 40 | image: grafana/loki:2.9.3 41 | command: -config.file=/mnt/config/loki-config.yml 42 | volumes: 43 | - ./Configs/loki.yml:/mnt/config/loki-config.yml 44 | ports: 45 | - "127.0.0.1:3100:3100" 46 | networks: 47 | - backend 48 | 49 | grafana: 50 | image: grafana/grafana:10.2.3 51 | environment: 52 | - GF_AUTH_ANONYMOUS_ENABLED=true 53 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 54 | - GF_AUTH_DISABLE_LOGIN_FORM=true 55 | volumes: 56 | - ./Configs/grafana.ini:/etc/grafana/grafana.ini 57 | - ./Configs/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yaml 58 | - ./Configs/dashboard.yml:/etc/grafana/provisioning/dashboards/dashboard.yml 59 | - ./Configs/dotnet-otel-dashboard.json:/etc/grafana/provisioning/dashboards/dotnet-otel-dashboard.json 60 | - ./Configs/logs-dashboard.json:/etc/grafana/provisioning/dashboards/logs-dashboard.json 61 | ports: 62 | - "127.0.0.1:3000:3000" 63 | networks: 64 | - backend 65 | 66 | postgres: 67 | image: postgres:16 68 | environment: 69 | - POSTGRES_USER=postgres 70 | - POSTGRES_PASSWORD=admin 71 | ports: 72 | - "127.0.0.1:5432:5432" 73 | networks: 74 | - backend 75 | 76 | rabbitmq: 77 | image: rabbitmq:3.12-management 78 | environment: 79 | - RABBITMQ_DEFAULT_USER=admin 80 | - RABBITMQ_DEFAULT_PASS=password 81 | - RABBITMQ_DEFAULT_VHOST=sample-dotnet-otel 82 | ports: 83 | - "127.0.0.1:5672:5672" 84 | - "127.0.0.1:15672:15672" 85 | networks: 86 | - backend 87 | 88 | proxy-service: 89 | build: 90 | dockerfile: SampleDotNetOTEL.ProxyService/Dockerfile 91 | context: . 92 | image: sample-dotnet-otel-proxy-service 93 | environment: 94 | - ServiceName=proxy-service 95 | - BusinessServiceBaseUrl=http://business-service-1;http://business-service-2;http://business-service-3; 96 | - OTEL_EXPORTER_JAEGER_AGENT_HOST=jaeger 97 | - OTEL_EXPORTER_JAEGER_AGENT_PORT=6831 98 | - OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:14268/api/traces 99 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 100 | - ASPNETCORE_HTTP_PORTS=80 101 | ports: 102 | - "127.0.0.1:8080:80" 103 | networks: 104 | - backend 105 | depends_on: 106 | - rabbitmq 107 | 108 | business-service-1: 109 | build: 110 | dockerfile: SampleDotNetOTEL.BusinessService/Dockerfile 111 | context: . 112 | image: sample-dotnet-otel-business-service 113 | environment: 114 | - ServiceName=business-service-1 115 | - ConnectionStrings__Default=Server=postgres;Port=5432;Database=weather;User Id=postgres;Password=admin; 116 | - ErrorRate=0.95 117 | - OTEL_EXPORTER_JAEGER_AGENT_HOST=jaeger 118 | - OTEL_EXPORTER_JAEGER_AGENT_PORT=6831 119 | - OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:14268/api/traces 120 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 121 | - ASPNETCORE_HTTP_PORTS=80 122 | ports: 123 | - "127.0.0.1:8081:80" 124 | networks: 125 | - backend 126 | depends_on: 127 | - postgres 128 | - rabbitmq 129 | 130 | business-service-2: 131 | build: 132 | dockerfile: SampleDotNetOTEL.BusinessService/Dockerfile 133 | context: . 134 | image: sample-dotnet-otel-business-service 135 | environment: 136 | - ServiceName=business-service-2 137 | - ConnectionStrings__Default=Server=postgres;Port=5432;Database=weather;User Id=postgres;Password=admin; 138 | - OTEL_EXPORTER_JAEGER_AGENT_HOST=jaeger 139 | - OTEL_EXPORTER_JAEGER_AGENT_PORT=6831 140 | - OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:14268/api/traces 141 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 142 | - ASPNETCORE_HTTP_PORTS=80 143 | ports: 144 | - "127.0.0.1:8082:80" 145 | networks: 146 | - backend 147 | depends_on: 148 | - postgres 149 | - rabbitmq 150 | 151 | business-service-3: 152 | build: 153 | dockerfile: SampleDotNetOTEL.BusinessService/Dockerfile 154 | context: . 155 | image: sample-dotnet-otel-business-service 156 | environment: 157 | - ServiceName=business-service-3 158 | - ConnectionStrings__Default=Server=postgres;Port=5432;Database=weather;User Id=postgres;Password=admin; 159 | - OTEL_EXPORTER_JAEGER_AGENT_HOST=jaeger 160 | - OTEL_EXPORTER_JAEGER_AGENT_PORT=6831 161 | - OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:14268/api/traces 162 | - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 163 | - ASPNETCORE_HTTP_PORTS=80 164 | ports: 165 | - "127.0.0.1:8083:80" 166 | networks: 167 | - backend 168 | depends_on: 169 | - postgres 170 | - rabbitmq 171 | 172 | spammer: 173 | image: alpine/curl:latest 174 | entrypoint: 175 | - /bin/sh 176 | - -c 177 | - | 178 | sleep 5 179 | while true; do 180 | curl -s "http://proxy-service/weather" || true 181 | curl -s "http://proxy-service/hello" || true 182 | curl -s "http://proxy-service/hello?name=John" || true 183 | curl -s -X POST -H "Content-Type: application/json" -d "{\"message\": \"This is a test message send thought RabbitMQ ($$(date))\"}" "http://proxy-service/messages" || true 184 | sleep 0.25 185 | done 186 | networks: 187 | - backend 188 | depends_on: 189 | - proxy-service 190 | - business-service-1 191 | - business-service-2 192 | - business-service-3 193 | 194 | networks: 195 | backend: 196 | -------------------------------------------------------------------------------- /Diagram.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /Configs/grafana.ini: -------------------------------------------------------------------------------- 1 | ##################### Grafana Configuration Defaults ##################### 2 | # 3 | # Do not modify this file in grafana installs 4 | # 5 | 6 | # possible values : production, development 7 | app_mode = production 8 | 9 | # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty 10 | instance_name = ${HOSTNAME} 11 | 12 | # force migration will run migrations that might cause dataloss 13 | force_migration = false 14 | 15 | #################################### Paths ############################### 16 | [paths] 17 | # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) 18 | data = data 19 | 20 | # Temporary files in `data` directory older than given duration will be removed 21 | temp_data_lifetime = 24h 22 | 23 | # Directory where grafana can store logs 24 | logs = data/log 25 | 26 | # Directory where grafana will automatically scan and look for plugins 27 | plugins = data/plugins 28 | 29 | # folder that contains provisioning config files that grafana will apply on startup and while running. 30 | provisioning = conf/provisioning 31 | 32 | #################################### Server ############################## 33 | [server] 34 | # Protocol (http, https, h2, socket) 35 | protocol = http 36 | 37 | # The ip address to bind to, empty will bind to all interfaces 38 | http_addr = 39 | 40 | # The http port to use 41 | http_port = 3000 42 | 43 | # The public facing domain name used to access grafana from a browser 44 | domain = localhost 45 | 46 | # Redirect to correct domain if host header does not match domain 47 | # Prevents DNS rebinding attacks 48 | enforce_domain = false 49 | 50 | # The full public facing url 51 | root_url = %(protocol)s://%(domain)s:%(http_port)s/ 52 | 53 | # Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons. 54 | serve_from_sub_path = false 55 | 56 | # Log web requests 57 | router_logging = false 58 | 59 | # the path relative working path 60 | static_root_path = public 61 | 62 | # enable gzip 63 | enable_gzip = false 64 | 65 | # https certs & key file 66 | cert_file = 67 | cert_key = 68 | 69 | # Unix socket path 70 | socket = /tmp/grafana.sock 71 | 72 | # CDN Url 73 | cdn_url = 74 | 75 | # Sets the maximum time in minutes before timing out read of an incoming request and closing idle connections. 76 | # `0` means there is no timeout for reading the request. 77 | read_timeout = 0 78 | 79 | #################################### Database ############################ 80 | [database] 81 | # You can configure the database connection by specifying type, host, name, user and password 82 | # as separate properties or as on string using the url property. 83 | 84 | # Either "mysql", "postgres" or "sqlite3", it's your choice 85 | type = sqlite3 86 | host = 127.0.0.1:3306 87 | name = grafana 88 | user = root 89 | # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" 90 | password = 91 | # Use either URL or the previous fields to configure the database 92 | # Example: mysql://user:secret@host:port/database 93 | url = 94 | 95 | # Max idle conn setting default is 2 96 | max_idle_conn = 2 97 | 98 | # Max conn setting default is 0 (mean not set) 99 | max_open_conn = 100 | 101 | # Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) 102 | conn_max_lifetime = 14400 103 | 104 | # Set to true to log the sql calls and execution times. 105 | log_queries = 106 | 107 | # For "postgres", use either "disable", "require" or "verify-full" 108 | # For "mysql", use either "true", "false", or "skip-verify". 109 | ssl_mode = disable 110 | 111 | # Database drivers may support different transaction isolation levels. 112 | # Currently, only "mysql" driver supports isolation levels. 113 | # If the value is empty - driver's default isolation level is applied. 114 | # For "mysql" use "READ-UNCOMMITTED", "READ-COMMITTED", "REPEATABLE-READ" or "SERIALIZABLE". 115 | isolation_level = 116 | 117 | ca_cert_path = 118 | client_key_path = 119 | client_cert_path = 120 | server_cert_name = 121 | 122 | # For "sqlite3" only, path relative to data_path setting 123 | path = grafana.db 124 | 125 | # For "sqlite3" only. cache mode setting used for connecting to the database 126 | cache_mode = private 127 | 128 | # For "mysql" only if migrationLocking feature toggle is set. How many seconds to wait before failing to lock the database for the migrations, default is 0. 129 | locking_attempt_timeout_sec = 0 130 | 131 | #################################### Cache server ############################# 132 | [remote_cache] 133 | # Either "redis", "memcached" or "database" default is "database" 134 | type = database 135 | 136 | # cache connectionstring options 137 | # database: will use Grafana primary database. 138 | # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'. 139 | # memcache: 127.0.0.1:11211 140 | connstr = 141 | 142 | #################################### Data proxy ########################### 143 | [dataproxy] 144 | 145 | # This enables data proxy logging, default is false 146 | logging = false 147 | 148 | # How long the data proxy waits to read the headers of the response before timing out, default is 30 seconds. 149 | # This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set. 150 | timeout = 30 151 | 152 | # How long the data proxy waits to establish a TCP connection before timing out, default is 10 seconds. 153 | dialTimeout = 10 154 | 155 | # How many seconds the data proxy waits before sending a keepalive request. 156 | keep_alive_seconds = 30 157 | 158 | # How many seconds the data proxy waits for a successful TLS Handshake before timing out. 159 | tls_handshake_timeout_seconds = 10 160 | 161 | # How many seconds the data proxy will wait for a server's first response headers after 162 | # fully writing the request headers if the request has an "Expect: 100-continue" 163 | # header. A value of 0 will result in the body being sent immediately, without 164 | # waiting for the server to approve. 165 | expect_continue_timeout_seconds = 1 166 | 167 | # Optionally limits the total number of connections per host, including connections in the dialing, 168 | # active, and idle states. On limit violation, dials will block. 169 | # A value of zero (0) means no limit. 170 | max_conns_per_host = 0 171 | 172 | # The maximum number of idle connections that Grafana will keep alive. 173 | max_idle_connections = 100 174 | 175 | # How many seconds the data proxy keeps an idle connection open before timing out. 176 | idle_conn_timeout_seconds = 90 177 | 178 | # If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request. 179 | send_user_header = false 180 | 181 | # Limit the amount of bytes that will be read/accepted from responses of outgoing HTTP requests. 182 | response_limit = 0 183 | 184 | # Limits the number of rows that Grafana will process from SQL data sources. 185 | row_limit = 1000000 186 | 187 | #################################### Analytics ########################### 188 | [analytics] 189 | # Server reporting, sends usage counters to stats.grafana.org every 24 hours. 190 | # No ip addresses are being tracked, only simple counters to track 191 | # running instances, dashboard and error counts. It is very helpful to us. 192 | # Change this option to false to disable reporting. 193 | reporting_enabled = true 194 | 195 | # The name of the distributor of the Grafana instance. Ex hosted-grafana, grafana-labs 196 | reporting_distributor = grafana-labs 197 | 198 | # Set to false to disable all checks to https://grafana.com 199 | # for new versions of grafana. The check is used 200 | # in some UI views to notify that a grafana update exists. 201 | # This option does not cause any auto updates, nor send any information 202 | # only a GET request to https://raw.githubusercontent.com/grafana/grafana/main/latest.json to get the latest version. 203 | check_for_updates = true 204 | 205 | # Set to false to disable all checks to https://grafana.com 206 | # for new versions of plugins. The check is used 207 | # in some UI views to notify that a plugin update exists. 208 | # This option does not cause any auto updates, nor send any information 209 | # only a GET request to https://grafana.com to get the latest versions. 210 | check_for_plugin_updates = true 211 | 212 | # Google Analytics universal tracking code, only enabled if you specify an id here 213 | google_analytics_ua_id = 214 | 215 | # Google Analytics 4 tracking code, only enabled if you specify an id here 216 | google_analytics_4_id = 217 | 218 | # Google Tag Manager ID, only enabled if you specify an id here 219 | google_tag_manager_id = 220 | 221 | # Rudderstack write key, enabled only if rudderstack_data_plane_url is also set 222 | rudderstack_write_key = 223 | 224 | # Rudderstack data plane url, enabled only if rudderstack_write_key is also set 225 | rudderstack_data_plane_url = 226 | 227 | # Rudderstack SDK url, optional, only valid if rudderstack_write_key and rudderstack_data_plane_url is also set 228 | rudderstack_sdk_url = 229 | 230 | # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config 231 | rudderstack_config_url = 232 | 233 | # Application Insights connection string. Specify an URL string to enable this feature. 234 | application_insights_connection_string = 235 | 236 | # Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``. 237 | application_insights_endpoint_url = 238 | 239 | # Controls if the UI contains any links to user feedback forms 240 | feedback_links_enabled = true 241 | 242 | #################################### Security ############################ 243 | [security] 244 | # disable creation of admin user on first start of grafana 245 | disable_initial_admin_creation = false 246 | 247 | # default admin user, created on startup 248 | admin_user = admin 249 | 250 | # default admin password, can be changed before first start of grafana, or in profile settings 251 | admin_password = admin 252 | 253 | # default admin email, created on startup 254 | admin_email = admin@localhost 255 | 256 | # used for signing 257 | secret_key = SW2YcwTIb9zpOOhoPsMm 258 | 259 | # current key provider used for envelope encryption, default to static value specified by secret_key 260 | encryption_provider = secretKey.v1 261 | 262 | # list of configured key providers, space separated (Enterprise only): e.g., awskms.v1 azurekv.v1 263 | available_encryption_providers = 264 | 265 | # disable gravatar profile images 266 | disable_gravatar = false 267 | 268 | # data source proxy whitelist (ip_or_domain:port separated by spaces) 269 | data_source_proxy_whitelist = 270 | 271 | # disable protection against brute force login attempts 272 | disable_brute_force_login_protection = false 273 | 274 | # set to true if you host Grafana behind HTTPS. default is false. 275 | cookie_secure = false 276 | 277 | # set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled" 278 | cookie_samesite = lax 279 | 280 | # set to true if you want to allow browsers to render Grafana in a ,