diff --git a/.gitignore b/.gitignore index efff01f2..d8023b4d 100644 --- a/.gitignore +++ b/.gitignore @@ -261,5 +261,5 @@ __pycache__/ *.pyc .vscode - +.DS_Store appsettings.*.json \ No newline at end of file diff --git a/Lectures/Lecture_13/IW5_Lecture13-Deployment_Aspire.pptx b/Lectures/Lecture_13/IW5_Lecture13-Deployment_Aspire.pptx new file mode 100644 index 00000000..04d3c254 --- /dev/null +++ b/Lectures/Lecture_13/IW5_Lecture13-Deployment_Aspire.pptx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:591aef445da4a3f89ab5bd609782d8713c738707beabfede43125dee0fe2cd64 +size 3388756 diff --git a/src/CookBook/.dockerignore b/src/CookBook/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/src/CookBook/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/CookBook/CookBook.Api.App/CookBook.Api.App.csproj b/src/CookBook/CookBook.Api.App/CookBook.Api.App.csproj index 516e0ffb..70f618c5 100644 --- a/src/CookBook/CookBook.Api.App/CookBook.Api.App.csproj +++ b/src/CookBook/CookBook.Api.App/CookBook.Api.App.csproj @@ -2,6 +2,7 @@ 70e22f70-c609-4d6a-9be8-b462915b90e5 + Linux @@ -20,8 +21,14 @@ + + + + .dockerignore + + diff --git a/src/CookBook/CookBook.Api.App/Dockerfile b/src/CookBook/CookBook.Api.App/Dockerfile new file mode 100644 index 00000000..ac2b4f65 --- /dev/null +++ b/src/CookBook/CookBook.Api.App/Dockerfile @@ -0,0 +1,31 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY Directory.Build.props . +COPY ["CookBook.Api.App/CookBook.Api.App.csproj", "CookBook.Api.App/"] +COPY ["CookBook.Api.BL/CookBook.Api.BL.csproj", "CookBook.Api.BL/"] +COPY ["CookBook.Api.DAL.Common/CookBook.Api.DAL.Common.csproj", "CookBook.Api.DAL.Common/"] +COPY ["CookBook.Common/CookBook.Common.csproj", "CookBook.Common/"] +COPY ["CookBook.Common.BL/CookBook.Common.BL.csproj", "CookBook.Common.BL/"] +COPY ["CookBook.Common.Models/CookBook.Common.Models.csproj", "CookBook.Common.Models/"] +COPY ["CookBook.Api.DAL.EF/CookBook.Api.DAL.EF.csproj", "CookBook.Api.DAL.EF/"] +COPY ["CookBook.Api.DAL.Memory/CookBook.Api.DAL.Memory.csproj", "CookBook.Api.DAL.Memory/"] +COPY ["CookBook.Hosting.ServiceDefaults/CookBook.Hosting.ServiceDefaults.csproj", "CookBook.Hosting.ServiceDefaults/"] +RUN dotnet restore "CookBook.Api.App/CookBook.Api.App.csproj" +COPY . . +WORKDIR "/src/CookBook.Api.App" +RUN dotnet build "CookBook.Api.App.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "CookBook.Api.App.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "CookBook.Api.App.dll"] diff --git a/src/CookBook/CookBook.Api.App/Program.cs b/src/CookBook/CookBook.Api.App/Program.cs index edb4f316..9da0e3d3 100644 --- a/src/CookBook/CookBook.Api.App/Program.cs +++ b/src/CookBook/CookBook.Api.App/Program.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using AutoMapper; -using AutoMapper.Internal; using CookBook.Api.App.Extensions; using CookBook.Api.App.Processors; using CookBook.Api.BL.Facades; @@ -15,18 +12,13 @@ using CookBook.Common.Extensions; using CookBook.Common.Models; using CookBook.Common.Resources; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using CookBook.Hosting.ServiceDefaults; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Localization; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Localization; var builder = WebApplication.CreateBuilder(); +builder.AddServiceDefaults(); ConfigureCors(builder.Services); ConfigureLocalization(builder.Services); @@ -36,6 +28,7 @@ ConfigureAutoMapper(builder.Services); var app = builder.Build(); +app.MapDefaultEndpoints(); ValidateAutoMapperConfiguration(app.Services); diff --git a/src/CookBook/CookBook.API.DAL.IntegrationTests/CookBook.API.DAL.IntegrationTests.csproj b/src/CookBook/CookBook.Api.DAL.IntegrationTests/CookBook.Api.DAL.IntegrationTests.csproj similarity index 100% rename from src/CookBook/CookBook.API.DAL.IntegrationTests/CookBook.API.DAL.IntegrationTests.csproj rename to src/CookBook/CookBook.Api.DAL.IntegrationTests/CookBook.Api.DAL.IntegrationTests.csproj diff --git a/src/CookBook/CookBook.API.DAL.IntegrationTests/IDatabaseFixture.cs b/src/CookBook/CookBook.Api.DAL.IntegrationTests/IDatabaseFixture.cs similarity index 91% rename from src/CookBook/CookBook.API.DAL.IntegrationTests/IDatabaseFixture.cs rename to src/CookBook/CookBook.Api.DAL.IntegrationTests/IDatabaseFixture.cs index 237afc2c..25da565e 100644 --- a/src/CookBook/CookBook.API.DAL.IntegrationTests/IDatabaseFixture.cs +++ b/src/CookBook/CookBook.Api.DAL.IntegrationTests/IDatabaseFixture.cs @@ -3,7 +3,7 @@ using CookBook.Api.DAL.Common.Entities; using CookBook.Api.DAL.Common.Repositories; -namespace CookBook.API.DAL.IntegrationTests; +namespace CookBook.Api.DAL.IntegrationTests; public interface IDatabaseFixture { diff --git a/src/CookBook/CookBook.API.DAL.IntegrationTests/InMemoryDatabaseFixture.cs b/src/CookBook/CookBook.Api.DAL.IntegrationTests/InMemoryDatabaseFixture.cs similarity index 98% rename from src/CookBook/CookBook.API.DAL.IntegrationTests/InMemoryDatabaseFixture.cs rename to src/CookBook/CookBook.Api.DAL.IntegrationTests/InMemoryDatabaseFixture.cs index 9fb6e754..5935adde 100644 --- a/src/CookBook/CookBook.API.DAL.IntegrationTests/InMemoryDatabaseFixture.cs +++ b/src/CookBook/CookBook.Api.DAL.IntegrationTests/InMemoryDatabaseFixture.cs @@ -8,7 +8,7 @@ using CookBook.Common.Enums; using Newtonsoft.Json; -namespace CookBook.API.DAL.IntegrationTests; +namespace CookBook.Api.DAL.IntegrationTests; public class InMemoryDatabaseFixture : IDatabaseFixture { diff --git a/src/CookBook/CookBook.API.DAL.IntegrationTests/RecipeRepositoryTests.cs b/src/CookBook/CookBook.Api.DAL.IntegrationTests/RecipeRepositoryTests.cs similarity index 99% rename from src/CookBook/CookBook.API.DAL.IntegrationTests/RecipeRepositoryTests.cs rename to src/CookBook/CookBook.Api.DAL.IntegrationTests/RecipeRepositoryTests.cs index 55e52d78..0013763d 100644 --- a/src/CookBook/CookBook.API.DAL.IntegrationTests/RecipeRepositoryTests.cs +++ b/src/CookBook/CookBook.Api.DAL.IntegrationTests/RecipeRepositoryTests.cs @@ -5,7 +5,7 @@ using CookBook.Common.Enums; using Xunit; -namespace CookBook.API.DAL.IntegrationTests; +namespace CookBook.Api.DAL.IntegrationTests; public class RecipeRepositoryTests { diff --git a/src/CookBook/CookBook.Hosting.AppHost/.config/dotnet-tools.json b/src/CookBook/CookBook.Hosting.AppHost/.config/dotnet-tools.json new file mode 100644 index 00000000..4c9769d1 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": false, + "tools": { + "aspirate": { + "version": "8.0.7", + "commands": [ + "aspirate" + ] + } + } +} \ No newline at end of file diff --git a/src/CookBook/CookBook.Hosting.AppHost/CookBook.Hosting.AppHost.csproj b/src/CookBook/CookBook.Hosting.AppHost/CookBook.Hosting.AppHost.csproj new file mode 100644 index 00000000..30d4c001 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/CookBook.Hosting.AppHost.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + true + 65e3f962-7661-40d1-b440-1a322d8d8dba + + + + + + + + + + + + + diff --git a/src/CookBook/CookBook.Hosting.AppHost/Program.cs b/src/CookBook/CookBook.Hosting.AppHost/Program.cs new file mode 100644 index 00000000..be9f63db --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/Program.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Configuration; + +var builder = DistributedApplication.CreateBuilder(args); +builder.Configuration.AddEnvironmentVariables(); + +var prometheus = builder.AddContainer("prometheus", "prom/prometheus", "v2.53.2") + .WithHttpEndpoint(targetPort: 9090, isProxied: false) + .WithBindMount("./configs/prometheus/config.yaml", "/mnt/config/local-config.yaml", isReadOnly: true) + .WithArgs("--config.file=/mnt/config/local-config.yaml", "--web.enable-remote-write-receiver"); + +var grafanaLoki = builder.AddContainer("grafanaLoki", "grafana/loki", "3.2.0") + .WithHttpEndpoint(targetPort: 3100, isProxied: false) + .WithBindMount("./configs/loki/config.yaml", "/mnt/config/local-config.yaml", isReadOnly: true) + .WithArgs("-config.file=/mnt/config/local-config.yaml"); + +var grafanaTempo = builder.AddContainer("grafanaTempo", "grafana/tempo", "2.6.0") + .WithHttpEndpoint(targetPort: 3200, isProxied: false) + .WithEndpoint(targetPort: 9097, name: "grpc", isProxied: false) + .WithBindMount("./configs/tempo/config.yaml", "/mnt/config/local-config.yaml", isReadOnly: true) + .WithArgs("-config.file=/mnt/config/local-config.yaml"); + +var grafana = builder.AddContainer("grafana", "grafana/grafana", "11.2.0") + .WithHttpEndpoint(targetPort: 3000) + .WithBindMount("./configs/grafana/config", "/etc/grafana", isReadOnly: true) + .WithBindMount("./configs/grafana/dashboards", "/var/lib/grafana/dashboards", isReadOnly: true) + .WithVolume("grafana-data", "/var/lib/grafana") + .WithReference(grafanaTempo.GetEndpoint("http")) + .WithReference(prometheus.GetEndpoint("http")) + .WithReference(grafanaLoki.GetEndpoint("http")); + +var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector", "0.109.0") + .WithEndpoint(targetPort: 4317, name: "grpc", scheme: "http", isProxied: false) + .WithEndpoint(targetPort: 4318, name: "http", scheme: "http", isProxied: false) + .WithBindMount("./configs/otel-collector/config.yaml", "/etc/otelcol/config.yaml", isReadOnly: true) + .WithEnvironment("GRAFANA_TEMPO_GRPC", () => + { + var endpoint = grafanaTempo.GetEndpoint("grpc"); + return $"{endpoint.ContainerHost}:{endpoint.TargetPort}"; + }) + .WithEnvironment(context => + { + if (builder.Configuration.GetValue("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL") is {} otlpEndpoint) + { + var endpointUrl = new HostUrl(otlpEndpoint); + context.EnvironmentVariables["DOTNET_DASHBOARD_OTLP_ENDPOINT"] = endpointUrl; + } + }) + .WithReference(grafanaTempo.GetEndpoint("grpc")) + .WithReference(prometheus.GetEndpoint("http")) + .WithReference(grafanaLoki.GetEndpoint("http")); + +var api = builder.AddProject("api") + .WithHttpEndpoint(); + +var web = builder.AddProject("web"); + +var gateway = builder.AddProject("gateway") + .WithReference(api) + .WithReference(web) + .WithHttpEndpoint() + .WithExternalHttpEndpoints(); + + +if (builder.ExecutionContext.IsPublishMode) +{ + web.WithHttpEndpoint(targetPort: 8080, env: "HTTP_PORT"); +} +else +{ + web.WithHttpEndpoint(); +} + +ConfigureOpenTelemetryExporter(api); +ConfigureOpenTelemetryExporter(gateway); + +builder.Build().Run(); +return; + +void ConfigureOpenTelemetryExporter(IResourceBuilder resourceBuilder) where T : IResourceWithEnvironment +{ + if (builder.Configuration.GetValue("SendMetricsToCollector")) + { + var grpcEndpoint = otelCollector.GetEndpoint("grpc"); + resourceBuilder.WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", grpcEndpoint); + resourceBuilder.WithEnvironment("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc"); + } + else + { + // sends metrics to dashboard + resourceBuilder.WithOtlpExporter(); + } +} diff --git a/src/CookBook/CookBook.Hosting.AppHost/Properties/launchSettings.json b/src/CookBook/CookBook.Hosting.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..24c7eaf4 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15079", + "environmentVariables": { + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", +// "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS": "true", + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19066", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20096" + } + } + } +} diff --git a/src/CookBook/CookBook.Hosting.AppHost/README.md b/src/CookBook/CookBook.Hosting.AppHost/README.md new file mode 100644 index 00000000..0b7d7f5d --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/README.md @@ -0,0 +1,20 @@ +# .NET Aspire AppHost + +## Install / Restore +```sh +dotnet workload install aspire +dotnet tool install -g aspirate +``` + +```sh +dotnet workload update +dotnet tool restore +``` + +## Generate output +Based on already existing `aspirate-state.json` this command will generate a Docker Compose file along with program images. +These will be stored locally. + +```shell +aspirate generate +``` diff --git a/src/CookBook/CookBook.Hosting.AppHost/appsettings.json b/src/CookBook/CookBook.Hosting.AppHost/appsettings.json new file mode 100644 index 00000000..4e4ded73 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/appsettings.json @@ -0,0 +1,15 @@ +{ + "SendMetricsToCollector": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Dashboard": { + "Otlp": { + "AuthMode": "Unsecured" + } + } +} diff --git a/src/CookBook/CookBook.Hosting.AppHost/aspirate-output/docker-compose.yaml b/src/CookBook/CookBook.Hosting.AppHost/aspirate-output/docker-compose.yaml new file mode 100644 index 00000000..d4331cf8 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/aspirate-output/docker-compose.yaml @@ -0,0 +1,42 @@ +services: + api: + container_name: "api" + image: "iw5/api:latest" + environment: + OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true" + OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true" + OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory" + ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" + HTTP_PORTS: "8080" + ports: + - target: 8080 + published: 10000 + restart: unless-stopped + web: + container_name: "web" + image: "iw5/web:latest" + environment: + OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true" + OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true" + OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory" + ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" + HTTP_PORT: "8080" + ports: + - target: 8080 + published: 10001 + restart: unless-stopped + gateway: + container_name: "gateway" + image: "iw5/gateway:latest" + environment: + OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true" + OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true" + OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory" + ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" + HTTP_PORTS: "8080" + services__api__http__0: "http://api:8080" + services__web__http__0: "http://web:8080" + ports: + - target: 8080 + published: 10002 + restart: unless-stopped diff --git a/src/CookBook/CookBook.Hosting.AppHost/aspirate-state.json b/src/CookBook/CookBook.Hosting.AppHost/aspirate-state.json new file mode 100644 index 00000000..599c8c25 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/aspirate-state.json @@ -0,0 +1,13 @@ +{ + "projectPath": ".", + "outputPath": "aspirate-output", + "containerImageTags": [ + "latest" + ], + "containerBuilder": "docker", + "containerRepositoryPrefix": "iw5", + "outputFormat": "compose", + "disableSecrets": true, + "privateRegistryEmail": "aspir8@aka.ms", + "processAllComponents": true +} \ No newline at end of file diff --git a/src/CookBook/CookBook.Hosting.AppHost/aspirate.json b/src/CookBook/CookBook.Hosting.AppHost/aspirate.json new file mode 100644 index 00000000..b449d196 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/aspirate.json @@ -0,0 +1,11 @@ +{ + "TemplatePath": null, + "ContainerSettings": { + "Registry": null, + "RepositoryPrefix": "iw5", + "Tags": [ + "latest" + ], + "Builder": null + } +} \ No newline at end of file diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/grafana.ini b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/grafana.ini new file mode 100644 index 00000000..967b2a47 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/grafana.ini @@ -0,0 +1,16 @@ +[auth.anonymous] +enabled = true + +# Organization name that should be used for unauthenticated users +org_name = IW5 + +# Role for unauthenticated users, other valid values are `Editor` and `Admin` +org_role = Admin + +# Hide the Grafana version text from the footer and help tooltip for unauthenticated users (default: false) +hide_version = false + +[dashboards] +default_home_dashboard_path = /var/lib/grafana/dashboards/aspnetcore.json + +min_refresh_interval = 1s diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/provisioning/dashboards/default.yaml b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/provisioning/dashboards/default.yaml new file mode 100644 index 00000000..316b5f76 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/provisioning/dashboards/default.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +providers: + - name: Default + folder: .NET + type: file + options: + path: + /var/lib/grafana/dashboards diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/provisioning/datasources/default.yaml b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/provisioning/datasources/default.yaml new file mode 100644 index 00000000..45c377eb --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/config/provisioning/datasources/default.yaml @@ -0,0 +1,45 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: $__env{services__prometheus__http__0} + uid: prometheus + isDefault: true + - name: Loki + type: loki + access: proxy + url: $__env{services__grafanaLoki__http__0} + uid: loki + jsonData: + timeout: 300 + maxLines: 100000 + - name: Tempo + type: tempo + access: proxy + url: $__env{services__grafanaTempo__http__0} + uid: tempo + basicAuth: false + jsonData: + serviceMap: + datasourceUid: 'prometheus' + tracesToLogsV2: + datasourceUid: 'loki' + spanStartTimeShift: '-1h' + spanEndTimeShift: '1h' + tracesToMetrics: + datasourceUid: 'prometheus' + spanStartTimeShift: '1h' + spanEndTimeShift: '-1h' + nodeGraph: + enabled: true + search: + hide: false + traceQuery: + timeShiftEnabled: true + spanStartTimeShift: '1h' + spanEndTimeShift: '-1h' + spanBar: + type: 'Tag' + tag: 'url.path' diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/dashboards/aspnetcore.json b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/dashboards/aspnetcore.json new file mode 100644 index 00000000..c5131fa9 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/grafana/dashboards/aspnetcore.json @@ -0,0 +1,2068 @@ +{ + "__inputs": [], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.1.4" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Shows ASP.NET metrics from OpenTelemetry NuGet", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 15, + "panels": [], + "title": "Process", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 90, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "system" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "user" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 11, + "x": 0, + "y": 1 + }, + "id": 19, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(process_cpu_time_seconds_total{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "__auto", + "range": true, + "refId": "CPU Usage" + } + ], + "title": "CPU Usage", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "state" + ], + "valueLabel": "state" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-green", + "mode": "fixed" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 90, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 10, + "x": 11, + "y": 1 + }, + "id": 16, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_memory_usage_bytes{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "Memory Usage", + "range": true, + "refId": "Memory Usage" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + }, + { + "color": "dark-yellow", + "value": 50 + }, + { + "color": "dark-orange", + "value": 100 + }, + { + "color": "dark-red", + "value": 150 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 3, + "x": 21, + "y": 1 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_threads{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "Threads", + "range": true, + "refId": "Threads" + } + ], + "title": "Threads", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 2, + "panels": [], + "title": "Runtime", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd", + "seriesBy": "max" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 11 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_runtime_dotnet_gc_committed_memory_size_bytes{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "hide": false, + "legendFormat": "Committed Memory Size", + "range": true, + "refId": "Committed Memory Size" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_runtime_dotnet_gc_objects_size_bytes{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "Objects Size", + "range": true, + "refId": "Objects Size" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "irate(process_runtime_dotnet_gc_allocations_size_bytes_total{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "Allocations Size", + "range": true, + "refId": "Allocations Size" + } + ], + "title": "General Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "gen0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gen1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gen2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "loh" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "poh" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 11 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_runtime_dotnet_gc_heap_size_bytes{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "__auto", + "range": true, + "refId": "Heap Size" + } + ], + "title": "Heap Generations (bytes)", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "generation" + ], + "valueLabel": "generation" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "gen0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gen1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gen2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "loh" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "poh" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 11 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_runtime_dotnet_gc_heap_fragmentation_size_bytes{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "__auto", + "range": true, + "refId": "Heap Fragmentation" + } + ], + "title": "Heap Fragmentation (bytes)", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "generation" + ], + "valueLabel": "generation" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 1, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [ + { + "options": { + "0": { + "color": "transparent", + "index": 0, + "text": "None" + } + }, + "type": "value" + } + ], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "gen0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gen1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gen2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 20 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.3.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "idelta(process_runtime_dotnet_gc_collections_count_total{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "gc" + } + ], + "title": "GC Collections", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "generation" + ], + "mode": "columns", + "valueLabel": "generation" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-red", + "mode": "fixed" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 90, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 20 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "increase(process_runtime_dotnet_exceptions_count_total{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "Exceptions", + "range": true, + "refId": "Exceptions" + } + ], + "title": "Exceptions", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-green", + "mode": "fixed" + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 16, + "y": 20 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_runtime_dotnet_thread_pool_threads_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "ThreadPool Threads", + "range": true, + "refId": "ThreadPool Threads" + } + ], + "title": "ThreadPool Threads", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-green", + "mode": "fixed" + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 20, + "y": 20 + }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max_over_time(process_runtime_dotnet_thread_pool_queue_length{job=\"$job\", instance=\"$instance\"}[$__rate_interval])", + "legendFormat": "ThreadPool Threads Queue Length", + "range": true, + "refId": "ThreadPool Threads Queue Length" + } + ], + "title": "ThreadPool Threads Queue Length", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 33, + "panels": [], + "title": "HTTP Server", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-green", + "mode": "continuous-GrYlRd", + "seriesBy": "max" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 40, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "legendFormat": "p50", + "range": true, + "refId": "p50" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.75, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p75", + "range": true, + "refId": "p75" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p90", + "range": true, + "refId": "p90" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p95", + "range": true, + "refId": "p95" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.98, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p98", + "range": true, + "refId": "p98" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p99", + "range": true, + "refId": "p99" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.999, sum(rate(http_server_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p99.9", + "range": true, + "refId": "p99.9" + } + ], + "title": "Responses Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic", + "seriesBy": "max" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "All" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "4XX" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "5XX" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 47, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_server_duration_milliseconds_count{job=\"$job\", instance=\"$instance\", http_status_code!~\"2..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_server_duration_milliseconds_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval]))", + "legendFormat": "All", + "range": true, + "refId": "All" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_server_duration_milliseconds_count{job=\"$job\", instance=\"$instance\", http_status_code=~\"4..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_server_duration_milliseconds_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval]))", + "hide": false, + "legendFormat": "4XX", + "range": true, + "refId": "4XX" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_server_duration_milliseconds_count{job=\"$job\", instance=\"$instance\", http_status_code=~\"5..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_server_duration_milliseconds_count{job=\"$job\", instance=\"$instance\"}[$__rate_interval]))", + "hide": false, + "legendFormat": "5XX", + "range": true, + "refId": "5XX" + } + ], + "title": "Errors Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 21, + "panels": [], + "repeat": "http_client_peer_name", + "repeatDirection": "h", + "title": "HTTP Client ($http_client_peer_name)", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-green", + "mode": "continuous-GrYlRd", + "seriesBy": "max" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 23, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "legendFormat": "p50", + "range": true, + "refId": "p50" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.75, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p75", + "range": true, + "refId": "p75" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p90", + "range": true, + "refId": "p90" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p95", + "range": true, + "refId": "p95" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.98, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p98", + "range": true, + "refId": "p98" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p99", + "range": true, + "refId": "p99" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.999, sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "p99.9", + "range": true, + "refId": "p99.9" + } + ], + "title": "Requests Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic", + "seriesBy": "max" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "All" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "4XX" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "5XX" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 25, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\", http_status_code!~\"2..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval]))", + "legendFormat": "All", + "range": true, + "refId": "All" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\", http_status_code=~\"4..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval]))", + "hide": false, + "legendFormat": "4XX", + "range": true, + "refId": "4XX" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\", http_status_code=~\"5..\"}[$__rate_interval]) or vector(0)) / sum(rate(http_client_duration_milliseconds_bucket{job=\"$job\", instance=\"$instance\", net_peer_name=\"$http_client_peer_name\"}[$__rate_interval]))", + "hide": false, + "legendFormat": "5XX", + "range": true, + "refId": "5XX" + } + ], + "title": "Errors Rate", + "type": "timeseries" + } + ], + "refresh": "1m", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "dotnet", + "opentelemetry", + "otel", + "prometheus", + "process", + "runtime", + "httpclient", + "aspnetcore" + ], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(process_runtime_dotnet_gc_collections_count_total,job)", + "hide": 0, + "includeAll": false, + "label": "Job", + "multi": false, + "name": "job", + "options": [], + "query": { + "query": "label_values(process_runtime_dotnet_gc_collections_count_total,job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(process_runtime_dotnet_gc_collections_count_total{job=~\"$job\"},instance)", + "hide": 0, + "includeAll": false, + "label": "Instance", + "multi": false, + "name": "instance", + "options": [], + "query": { + "query": "label_values(process_runtime_dotnet_gc_collections_count_total{job=~\"$job\"},instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(http_client_duration_milliseconds_bucket{job=~\"$job\", instance=~\"$instance\"},net_peer_name)", + "hide": 2, + "includeAll": true, + "label": "HTTP Client Pear Name", + "multi": false, + "name": "http_client_peer_name", + "options": [], + "query": { + "query": "label_values(http_client_duration_milliseconds_bucket{job=~\"$job\", instance=~\"$instance\"},net_peer_name)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "type": "query" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ASP.NET OTEL Metrics", + "uid": "KdDACDp4z", + "version": 1, + "weekStart": "", + "gnetId": 17706 +} \ No newline at end of file diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/loki/config.yaml b/src/CookBook/CookBook.Hosting.AppHost/configs/loki/config.yaml new file mode 100644 index 00000000..7be6dd30 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/loki/config.yaml @@ -0,0 +1,45 @@ +limits_config: + allow_structured_metadata: true + max_entries_limit_per_query: 1000000 + +auth_enabled: false + +server: + http_listen_port: 3100 + log_level: debug + grpc_server_max_concurrent_streams: 1000 + +common: + instance_addr: 127.0.0.1 + path_prefix: /tmp/loki + storage: + filesystem: + chunks_directory: /tmp/loki/chunks + rules_directory: /tmp/loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 + +analytics: + reporting_enabled: false diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/otel-collector/config.yaml b/src/CookBook/CookBook.Hosting.AppHost/configs/otel-collector/config.yaml new file mode 100644 index 00000000..096cd7a3 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/otel-collector/config.yaml @@ -0,0 +1,63 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + +exporters: + otlp/aspire: + endpoint: ${env:DOTNET_DASHBOARD_OTLP_ENDPOINT} + tls: + insecure: true + + # Grafana Tempo -- traces + otlp/tempo: + endpoint: ${env:GRAFANA_TEMPO_GRPC} + tls: + insecure: true + + # Grafana Loki -- logs + otlphttp/loki: + endpoint: ${env:services__grafanaLoki__http__0}/otlp + + # Grafana Mimir -- metrics + prometheusremotewrite/prometheus: + endpoint: ${env:services__prometheus__http__0}/api/v1/write + +extensions: + health_check: + pprof: + zpages: + +service: + extensions: [health_check, pprof, zpages] + pipelines: + traces: + receivers: + - otlp + processors: + - batch + exporters: + - otlp/aspire + - otlp/tempo + metrics: + receivers: + - otlp + processors: + - batch + exporters: + - otlp/aspire + - prometheusremotewrite/prometheus + logs: + receivers: + - otlp + processors: + - batch + exporters: + - otlp/aspire + - otlphttp/loki diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/prometheus/config.yaml b/src/CookBook/CookBook.Hosting.AppHost/configs/prometheus/config.yaml new file mode 100644 index 00000000..bcef49a6 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/prometheus/config.yaml @@ -0,0 +1,4 @@ +global: + scrape_interval: 10s + +scrape_configs: [] diff --git a/src/CookBook/CookBook.Hosting.AppHost/configs/tempo/config.yaml b/src/CookBook/CookBook.Hosting.AppHost/configs/tempo/config.yaml new file mode 100644 index 00000000..5dccde85 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/configs/tempo/config.yaml @@ -0,0 +1,53 @@ +stream_over_http_enabled: true +server: + http_listen_port: 3200 + log_level: info + +query_frontend: + search: + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + trace_by_id: + duration_slo: 5s + +distributor: + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:9097 + +ingester: + max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally + +compactor: + compaction: + block_retention: 1h # overall Tempo trace retention. set for demo purposes + +metrics_generator: + registry: + external_labels: + source: tempo + cluster: docker-compose + storage: + path: /var/tempo/generator/wal + remote_write: [] +# remote_write: +# - url: http://prometheus:9090/api/v1/write +# send_exemplars: true + traces_storage: + path: /var/tempo/generator/traces + +storage: + trace: + backend: local # backend configuration to use + wal: + path: /var/tempo/wal # where to store the wal locally + local: + path: /var/tempo/blocks + +overrides: + defaults: + metrics_generator: + processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator + generate_native_histograms: both diff --git a/src/CookBook/CookBook.Hosting.AppHost/manifest.json b/src/CookBook/CookBook.Hosting.AppHost/manifest.json new file mode 100644 index 00000000..8ebaf94a --- /dev/null +++ b/src/CookBook/CookBook.Hosting.AppHost/manifest.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json.schemastore.org/aspire-8.0.json", + "resources": { + "api": { + "type": "project.v0", + "path": "../CookBook.Api.App/CookBook.Api.App.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{api.bindings.http.targetPort}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + } + } + }, + "web": { + "type": "project.v0", + "path": "../CookBook.Web.App/CookBook.Web.App.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORT": "{web.bindings.http.targetPort}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 8080 + } + } + }, + "gateway": { + "type": "project.v0", + "path": "../CookBook.Hosting.Gateway/CookBook.Hosting.Gateway.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{gateway.bindings.http.targetPort}", + "services__api__http__0": "{api.bindings.http.url}", + "services__web__http__0": "{web.bindings.http.url}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "external": true + } + } + } + } +} \ No newline at end of file diff --git a/src/CookBook/CookBook.Hosting.Gateway/CookBook.Hosting.Gateway.csproj b/src/CookBook/CookBook.Hosting.Gateway/CookBook.Hosting.Gateway.csproj new file mode 100644 index 00000000..ecbd0c19 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.Gateway/CookBook.Hosting.Gateway.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/CookBook/CookBook.Hosting.Gateway/Program.cs b/src/CookBook/CookBook.Hosting.Gateway/Program.cs new file mode 100644 index 00000000..e97c8c87 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.Gateway/Program.cs @@ -0,0 +1,19 @@ +using CookBook.Hosting.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); + +builder.Configuration.AddEnvironmentVariables(); + +builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetRequiredSection("ReverseProxy")) + .AddServiceDiscoveryDestinationResolver(); + +var app = builder.Build(); +app.MapDefaultEndpoints(); + +app.UseHttpsRedirection(); +app.UseRouting(); +app.MapReverseProxy(); + +app.Run(); diff --git a/src/CookBook/CookBook.Hosting.Gateway/Properties/launchSettings.json b/src/CookBook/CookBook.Hosting.Gateway/Properties/launchSettings.json new file mode 100644 index 00000000..4a4a803e --- /dev/null +++ b/src/CookBook/CookBook.Hosting.Gateway/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/CookBook/CookBook.Hosting.Gateway/appsettings.json b/src/CookBook/CookBook.Hosting.Gateway/appsettings.json new file mode 100644 index 00000000..9dd0137b --- /dev/null +++ b/src/CookBook/CookBook.Hosting.Gateway/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Yarp.ReverseProxy.Forwarder": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ReverseProxy": { + "Routes": { + "api-docs": { + "Order": 1, + "ClusterId": "api", + "Match": { + "Path": "/swagger/{**any}" + } + }, + "api": { + "Order": 2, + "ClusterId": "api", + "Match": { + "Path": "/api/{**any}" + } + }, + "web": { + "Order": 10, + "ClusterId": "web", + "Match": { + "Path": "/{**any}" + } + } + }, + "Clusters": { + "api": { + "Destinations": { + "api": { + "Address": "http://api", + "Health": "http://api/health" + } + } + }, + "web": { + "Destinations": { + "web": { + "Address": "http://web" + } + } + } + } + } +} diff --git a/src/CookBook/CookBook.Hosting.ServiceDefaults/CookBook.Hosting.ServiceDefaults.csproj b/src/CookBook/CookBook.Hosting.ServiceDefaults/CookBook.Hosting.ServiceDefaults.csproj new file mode 100644 index 00000000..1fdf70ac --- /dev/null +++ b/src/CookBook/CookBook.Hosting.ServiceDefaults/CookBook.Hosting.ServiceDefaults.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + diff --git a/src/CookBook/CookBook.Hosting.ServiceDefaults/Extensions.cs b/src/CookBook/CookBook.Hosting.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..4a12a6c5 --- /dev/null +++ b/src/CookBook/CookBook.Hosting.ServiceDefaults/Extensions.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace CookBook.Hosting.ServiceDefaults; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Configuration.AddEnvironmentVariables(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddProcessInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + // if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + // { + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + // } + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); + } + + return app; + } +} diff --git a/src/CookBook/CookBook.Web.App/CookBook.Web.App.csproj b/src/CookBook/CookBook.Web.App/CookBook.Web.App.csproj index 6fb6b927..00845592 100644 --- a/src/CookBook/CookBook.Web.App/CookBook.Web.App.csproj +++ b/src/CookBook/CookBook.Web.App/CookBook.Web.App.csproj @@ -1,4 +1,23 @@ + + + false + false + Linux + true + + + + + + linux-x64 + dolejskad/busybox:latest + + + + + + @@ -86,4 +105,10 @@ + + + .dockerignore + + + diff --git a/src/CookBook/CookBook.Web.App/Dockerfile b/src/CookBook/CookBook.Web.App/Dockerfile new file mode 100644 index 00000000..2a5c64e8 --- /dev/null +++ b/src/CookBook/CookBook.Web.App/Dockerfile @@ -0,0 +1,28 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY Directory.Build.props . +COPY ["CookBook.Web.App/CookBook.Web.App.csproj", "CookBook.Web.App/"] +COPY ["CookBook.Web.BL/CookBook.Web.BL.csproj", "CookBook.Web.BL/"] +COPY ["CookBook.Common.BL/CookBook.Common.BL.csproj", "CookBook.Common.BL/"] +COPY ["CookBook.Common.Models/CookBook.Common.Models.csproj", "CookBook.Common.Models/"] +COPY ["CookBook.Common/CookBook.Common.csproj", "CookBook.Common/"] +COPY ["CookBook.Web.DAL/CookBook.Web.DAL.csproj", "CookBook.Web.DAL/"] +RUN dotnet restore "CookBook.Web.App/CookBook.Web.App.csproj" +COPY . . +WORKDIR "/src/CookBook.Web.App" +RUN dotnet build "CookBook.Web.App.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "CookBook.Web.App.csproj" -c $BUILD_CONFIGURATION --self-contained -o /app/publish /p:UseAppHost=false + +FROM docker.io/library/busybox:1.36 AS final +WORKDIR /app +COPY --from=publish /app/publish . +CMD ["httpd", "-f", "-v", "-p", "8081", "-h", "./wwwroot"] diff --git a/src/CookBook/CookBook.Web.App/Program.cs b/src/CookBook/CookBook.Web.App/Program.cs index 85ea86cc..c4e33636 100644 --- a/src/CookBook/CookBook.Web.App/Program.cs +++ b/src/CookBook/CookBook.Web.App/Program.cs @@ -14,11 +14,11 @@ builder.Configuration.AddJsonFile("appsettings.json"); -var apiBaseUrl = builder.Configuration.GetValue("ApiBaseUrl"); +var apiBaseUrl = builder.Configuration.GetValue("ApiBaseUrl") ?? builder.HostEnvironment.BaseAddress; builder.Services.AddInstaller(); builder.Services.AddInstaller(apiBaseUrl); -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddAutoMapper(configuration => { // This is a temporary fix - should be able to remove this when version 11.0.2 comes out @@ -27,12 +27,7 @@ }, typeof(WebBLInstaller)); builder.Services.AddLocalization(); -builder.Services.Configure(options => -{ - options.IsLocalDbEnabled = bool.Parse(builder.Configuration.GetSection(nameof(LocalDbOptions))[nameof(LocalDbOptions.IsLocalDbEnabled)]); -}); - - -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.Configure(obj => builder.Configuration.GetRequiredSection(nameof(LocalDbOptions)) + .Bind(obj, opts => opts.ErrorOnUnknownConfiguration = true)); await builder.Build().RunAsync(); diff --git a/src/CookBook/CookBook.Web.App/Properties/launchSettings.json b/src/CookBook/CookBook.Web.App/Properties/launchSettings.json index 2a2c4a8d..dcd002e5 100644 --- a/src/CookBook/CookBook.Web.App/Properties/launchSettings.json +++ b/src/CookBook/CookBook.Web.App/Properties/launchSettings.json @@ -22,7 +22,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:44355;http://localhost:56567", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" } diff --git a/src/CookBook/CookBook.Web.App/wwwroot/appsettings.json b/src/CookBook/CookBook.Web.App/wwwroot/appsettings.json index df25a790..e95a4255 100644 --- a/src/CookBook/CookBook.Web.App/wwwroot/appsettings.json +++ b/src/CookBook/CookBook.Web.App/wwwroot/appsettings.json @@ -1,6 +1,6 @@ { - "ApiBaseUrl": "https://localhost:44378/", + // "ApiBaseUrl": "https://localhost:44378/", "LocalDbOptions": { "IsLocalDbEnabled": false } -} \ No newline at end of file +} diff --git a/src/CookBook/CookBook.sln b/src/CookBook/CookBook.sln index cb964a20..bf20c7ce 100644 --- a/src/CookBook/CookBook.sln +++ b/src/CookBook/CookBook.sln @@ -41,12 +41,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BL", "BL", "{4411F92C-9E5A- EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookBook.Api.BL.UnitTests", "CookBook.Api.BL.UnitTests\CookBook.Api.BL.UnitTests.csproj", "{89797A6C-B555-4C60-8D7C-D80B5024217D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookBook.API.DAL.IntegrationTests", "CookBook.API.DAL.IntegrationTests\CookBook.API.DAL.IntegrationTests.csproj", "{CDED4C89-4B14-4D77-8F6C-8E2889B706A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookBook.Api.DAL.IntegrationTests", "CookBook.Api.DAL.IntegrationTests\CookBook.Api.DAL.IntegrationTests.csproj", "{CDED4C89-4B14-4D77-8F6C-8E2889B706A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookBook.Api.App.EndToEndTests", "CookBook.Api.App.EndToEndTests\CookBook.Api.App.EndToEndTests.csproj", "{4087CC13-3279-4F7F-9723-4B86EC35FD39}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookBook.Web.App", "CookBook.Web.App\CookBook.Web.App.csproj", "{3E9D1C28-5A44-4AE2-B117-2DE376412B82}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{7B655E10-AB05-46B3-8CB0-61023BC04824}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CookBook.Hosting.AppHost", "CookBook.Hosting.AppHost\CookBook.Hosting.AppHost.csproj", "{F2DA881B-C58E-4ABC-AB53-5D02EAE4D9A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CookBook.Hosting.ServiceDefaults", "CookBook.Hosting.ServiceDefaults\CookBook.Hosting.ServiceDefaults.csproj", "{6AAB73E8-8731-4E11-9FB1-52C0748A53E2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CookBook.Hosting.Gateway", "CookBook.Hosting.Gateway\CookBook.Hosting.Gateway.csproj", "{1A3E6B4F-7447-4EB4-8169-A25559CA87E0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -109,6 +117,18 @@ Global {3E9D1C28-5A44-4AE2-B117-2DE376412B82}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E9D1C28-5A44-4AE2-B117-2DE376412B82}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E9D1C28-5A44-4AE2-B117-2DE376412B82}.Release|Any CPU.Build.0 = Release|Any CPU + {F2DA881B-C58E-4ABC-AB53-5D02EAE4D9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2DA881B-C58E-4ABC-AB53-5D02EAE4D9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2DA881B-C58E-4ABC-AB53-5D02EAE4D9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2DA881B-C58E-4ABC-AB53-5D02EAE4D9A7}.Release|Any CPU.Build.0 = Release|Any CPU + {6AAB73E8-8731-4E11-9FB1-52C0748A53E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AAB73E8-8731-4E11-9FB1-52C0748A53E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AAB73E8-8731-4E11-9FB1-52C0748A53E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AAB73E8-8731-4E11-9FB1-52C0748A53E2}.Release|Any CPU.Build.0 = Release|Any CPU + {1A3E6B4F-7447-4EB4-8169-A25559CA87E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A3E6B4F-7447-4EB4-8169-A25559CA87E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A3E6B4F-7447-4EB4-8169-A25559CA87E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A3E6B4F-7447-4EB4-8169-A25559CA87E0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +150,9 @@ Global {CDED4C89-4B14-4D77-8F6C-8E2889B706A0} = {DA7BE6FF-5E94-4000-9BD7-A7DA82EF224F} {4087CC13-3279-4F7F-9723-4B86EC35FD39} = {CE4F5F83-8F08-4F26-A46D-52370701657D} {3E9D1C28-5A44-4AE2-B117-2DE376412B82} = {777F05CE-DC73-46AE-BA0F-CCF368D381A1} + {F2DA881B-C58E-4ABC-AB53-5D02EAE4D9A7} = {7B655E10-AB05-46B3-8CB0-61023BC04824} + {6AAB73E8-8731-4E11-9FB1-52C0748A53E2} = {7B655E10-AB05-46B3-8CB0-61023BC04824} + {1A3E6B4F-7447-4EB4-8169-A25559CA87E0} = {7B655E10-AB05-46B3-8CB0-61023BC04824} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {10E5D329-1983-4C74-838B-CA0CFC4A2256}