diff --git a/.azure/applications/graphql/main.bicep b/.azure/applications/graphql/main.bicep index f655664ac..3fe306658 100644 --- a/.azure/applications/graphql/main.bicep +++ b/.azure/applications/graphql/main.bicep @@ -46,7 +46,7 @@ param appConfigurationName string param environmentKeyVaultName string var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { Environment: environment diff --git a/.azure/applications/service/main.bicep b/.azure/applications/service/main.bicep index 26812e1a8..fdf25a39c 100644 --- a/.azure/applications/service/main.bicep +++ b/.azure/applications/service/main.bicep @@ -71,7 +71,7 @@ param scale Scale = { } var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { Environment: environment Product: 'Dialogporten' diff --git a/.azure/applications/sync-resource-policy-information-job/main.bicep b/.azure/applications/sync-resource-policy-information-job/main.bicep index 740691b41..144c07ed0 100644 --- a/.azure/applications/sync-resource-policy-information-job/main.bicep +++ b/.azure/applications/sync-resource-policy-information-job/main.bicep @@ -32,7 +32,7 @@ param jobSchedule string param appInsightConnectionString string var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { FullName: '${namePrefix}-sync-resource-policy-information' Environment: environment diff --git a/.azure/applications/sync-subject-resource-mappings-job/main.bicep b/.azure/applications/sync-subject-resource-mappings-job/main.bicep index 3279e4d7e..2e743333b 100644 --- a/.azure/applications/sync-subject-resource-mappings-job/main.bicep +++ b/.azure/applications/sync-subject-resource-mappings-job/main.bicep @@ -32,7 +32,7 @@ param jobSchedule string param appInsightConnectionString string var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { FullName: '${namePrefix}-sync-subject-resource-mappings' Environment: environment diff --git a/.azure/applications/web-api-eu/main.bicep b/.azure/applications/web-api-eu/main.bicep index b4cbd6592..a2d141bdc 100644 --- a/.azure/applications/web-api-eu/main.bicep +++ b/.azure/applications/web-api-eu/main.bicep @@ -46,7 +46,7 @@ param appConfigurationName string param environmentKeyVaultName string var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { Environment: environment Product: 'Dialogporten' diff --git a/.azure/applications/web-api-migration-job/main.bicep b/.azure/applications/web-api-migration-job/main.bicep index ca5857455..4d1d66ebb 100644 --- a/.azure/applications/web-api-migration-job/main.bicep +++ b/.azure/applications/web-api-migration-job/main.bicep @@ -23,7 +23,7 @@ param containerAppEnvironmentName string param environmentKeyVaultName string var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { Environment: environment Product: 'Dialogporten' diff --git a/.azure/applications/web-api-so/main.bicep b/.azure/applications/web-api-so/main.bicep index eb1feccc0..9fd82a5ee 100644 --- a/.azure/applications/web-api-so/main.bicep +++ b/.azure/applications/web-api-so/main.bicep @@ -74,7 +74,7 @@ param scale Scale = { } var namePrefix = 'dp-be-${environment}' -var baseImageUrl = 'ghcr.io/digdir/dialogporten-' +var baseImageUrl = 'ghcr.io/altinn/dialogporten-' var tags = { Environment: environment Product: 'Dialogporten' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 39ac70abe..9508d55e3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ -* @digdir/team-dialogporten-backend -.azure/** @digdir/team-dialogporten-backend @digdir/team-dialogporten-infra -.github/** @digdir/team-dialogporten-backend @digdir/team-dialogporten-infra -.github/CODEOWNERS @digdir/team-dialogporten-backend \ No newline at end of file +* @altinn/team-dialogporten-backend +.azure/** @altinn/team-dialogporten-backend @altinn/team-dialogporten-infra +.github/** @altinn/team-dialogporten-backend @altinn/team-dialogporten-infra +.github/CODEOWNERS @altinn/team-dialogporten-backend \ No newline at end of file diff --git a/.github/workflows/ci-cd-main.yml b/.github/workflows/ci-cd-main.yml index d77025508..0072200ad 100644 --- a/.github/workflows/ci-cd-main.yml +++ b/.github/workflows/ci-cd-main.yml @@ -57,7 +57,7 @@ jobs: secrets: GCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} with: - dockerImageBaseName: ghcr.io/digdir/dialogporten- + dockerImageBaseName: ghcr.io/altinn/dialogporten- version: ${{ needs.get-current-version.outputs.version }}-${{ needs.generate-git-short-sha.outputs.gitShortSha }} deploy-infra: diff --git a/.github/workflows/ci-cd-staging.yml b/.github/workflows/ci-cd-staging.yml index 611839543..0099ce5a8 100644 --- a/.github/workflows/ci-cd-staging.yml +++ b/.github/workflows/ci-cd-staging.yml @@ -68,7 +68,7 @@ jobs: secrets: GCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} with: - dockerImageBaseName: ghcr.io/digdir/dialogporten- + dockerImageBaseName: ghcr.io/altinn/dialogporten- version: ${{ needs.get-current-version.outputs.version }} deploy-apps: diff --git a/.github/workflows/ci-cd-yt01.yml b/.github/workflows/ci-cd-yt01.yml index 3d13d7805..fe0b89722 100644 --- a/.github/workflows/ci-cd-yt01.yml +++ b/.github/workflows/ci-cd-yt01.yml @@ -38,7 +38,7 @@ jobs: secrets: GCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} with: - dockerImageBaseName: ghcr.io/digdir/dialogporten- + dockerImageBaseName: ghcr.io/altinn/dialogporten- version: ${{ needs.get-current-version.outputs.version }} deploy-infra: diff --git a/README.md b/README.md index 2b85f2a90..1a0318dca 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,12 @@ These health checks are integrated with Azure Container Apps' health probe syste ## Observability with OpenTelemetry -This project uses OpenTelemetry for distributed tracing and metrics collection. The setup includes: +This project uses OpenTelemetry for distributed tracing, metrics collection, and logging. The setup includes: ### Core Features - Distributed tracing across services - Runtime and application metrics +- Log aggregation and correlation - Integration with Azure Monitor/Application Insights - Support for both OTLP and Azure Monitor exporters - Automatic instrumentation for: @@ -157,15 +158,72 @@ OpenTelemetry is configured through environment variables that are automatically ### Local Development For local development, the project includes a docker-compose setup with: -- OpenTelemetry Collector -- Grafana -- Other supporting services +- OpenTelemetry Collector (ports 4317/4318 for OTLP receivers) +- Grafana (port 3000) +- Jaeger (port 16686) +- Loki (port 3100) +- Prometheus (port 9090) To run the local observability stack: ```bash podman compose -f docker-compose-otel.yml up ``` +### Accessing Observability Tools + +Once the local stack is running, you can access the following tools: + +#### Distributed Tracing with Jaeger +- URL: http://localhost:16686 +- Features: + - View distributed traces across services + - Search by service, operation, or trace ID + - Analyze timing and dependencies + - Debug request flows and errors + +#### Metrics with Prometheus +- URL: http://localhost:9090 +- Features: + - Query raw metrics data + - View metric targets and service discovery + - Debug metric collection + +#### Log Aggregation with Loki +- Direct URL: http://localhost:3100 +- Grafana Integration: http://localhost:3000 (preferred interface) +- Features: + - Search and filter logs across all services + - Correlate logs with traces using trace IDs + - Create log-based alerts and dashboards + - Use LogQL to query logs: + ```logql + # Example: Find all error logs + {container="web-api"} |= "error" + + # Example: Find logs with specific trace ID + {container=~"web-api|graphql"} |~ "trace_id=([a-f0-9]{32})" + ``` + +#### Metrics and Dashboards in Grafana +- URL: http://localhost:3000 +- Features: + - Pre-configured dashboards for: + - Application metrics + - Runtime metrics + - HTTP request metrics + - Data sources: + - Prometheus (metrics) + - Loki (logs) + - Jaeger (traces) + - Create custom dashboards + - Set up alerts + +#### OpenTelemetry Collector Endpoints +- OTLP gRPC receiver: localhost:4317 +- OTLP HTTP receiver: localhost:4318 +- Prometheus metrics: localhost:8888 +- Prometheus exporter metrics: localhost:8889 + ### Request Filtering The telemetry setup includes smart filtering to: @@ -252,7 +310,7 @@ For pull requests, the title must follow [Conventional Commits](https://www.conv The title of the PR will be used as the commit message when squashing/merging the pull request, and the body of the PR will be used as the description. This title will be used to generate the changelog (using [Release Please](https://github.com/google-github-actions/release-please-action)) -Using `fix` will add to "Bug Fixes", `feat` will add to "Features". All the others,`chore`, `ci`, etc., will be ignored. ([Example release](https://github.com/digdir/dialogporten/releases/tag/v1.12.0)) +Using `fix` will add to "Bug Fixes", `feat` will add to "Features". All the others,`chore`, `ci`, etc., will be ignored. ([Example release](https://github.com/altinn/dialogporten/releases/tag/v1.12.0)) ## Deployment diff --git a/docker-compose-otel.yml b/docker-compose-otel.yml index cb8f38aad..062f9f931 100644 --- a/docker-compose-otel.yml +++ b/docker-compose-otel.yml @@ -22,15 +22,36 @@ services: - "14250:14250" # Model used by collector environment: - COLLECTOR_OTLP_ENABLED=true + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "localhost:16686"] + interval: 3s + timeout: 3s + retries: 10 + start_period: 10s # Prometheus for metrics prometheus: - image: prom/prometheus:v3.0.1 + image: prom/prometheus:v3.1.0 volumes: - ./local-otel-configuration/prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090" + # Loki for log aggregation + loki: + image: grafana/loki:3.2.2 + ports: + - "3100:3100" + volumes: + - ./local-otel-configuration/loki-config.yaml:/etc/loki/local-config.yaml + command: -config.file=/etc/loki/local-config.yaml + healthcheck: + test: ["CMD-SHELL", "wget -q --tries=1 -O- http://localhost:3100/ready"] + interval: 3s + timeout: 3s + retries: 10 + start_period: 10s + # Grafana for metrics visualization grafana: image: grafana/grafana:11.4.0 @@ -43,3 +64,5 @@ services: - ./local-otel-configuration/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml - ./local-otel-configuration/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/dashboards.yml - ./local-otel-configuration/dashboards:/etc/grafana/provisioning/dashboards + depends_on: + - loki diff --git a/docs/schema/V1/package.json b/docs/schema/V1/package.json index 4704869d8..9155c26dc 100644 --- a/docs/schema/V1/package.json +++ b/docs/schema/V1/package.json @@ -8,7 +8,7 @@ "author": "DigDir", "main": "src/index.js", "repository": { - "url": "git+https://github.com/digdir/dialogporten.git" + "url": "git+https://github.com/Altinn/dialogporten.git" }, "scripts": { "build": "./gql-to-js.js", diff --git a/docs/schema/V1/swagger.verified.json b/docs/schema/V1/swagger.verified.json index 123733d29..308c14539 100644 --- a/docs/schema/V1/swagger.verified.json +++ b/docs/schema/V1/swagger.verified.json @@ -5838,7 +5838,7 @@ ] }, "post": { - "description": "The dialog is created with the given configuration. For more information see the documentation (link TBD).\n\nFor detailed information on validation rules, see [the source for CreateDialogCommandValidator](https://github.com/digdir/dialogporten/blob/main/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs)", + "description": "The dialog is created with the given configuration. For more information see the documentation (link TBD).\n\nFor detailed information on validation rules, see [the source for CreateDialogCommandValidator](https://github.com/altinn/dialogporten/blob/main/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs)", "operationId": "V1ServiceOwnerDialogsCreate_Dialog", "requestBody": { "content": { @@ -7036,4 +7036,4 @@ "url": "https://altinn-dev-api.azure-api.net/dialogporten" } ] -} +} \ No newline at end of file diff --git a/local-otel-configuration/dashboards/runtime-metrics.json b/local-otel-configuration/dashboards/runtime-metrics.json index eed1ef0c6..67c45c97e 100644 --- a/local-otel-configuration/dashboards/runtime-metrics.json +++ b/local-otel-configuration/dashboards/runtime-metrics.json @@ -85,13 +85,19 @@ "type": "timeseries", "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "Prometheus" - }, "expr": "dialogporten_process_runtime_dotnet_gc_heap_size_bytes", "legendFormat": "Heap Size", "refId": "A" + }, + { + "expr": "dialogporten_process_runtime_dotnet_gc_committed_memory_size_bytes", + "legendFormat": "Committed Memory", + "refId": "B" + }, + { + "expr": "dialogporten_dotnet_process_memory_working_set_bytes", + "legendFormat": "Working Set", + "refId": "C" } ] }, @@ -171,13 +177,14 @@ "type": "timeseries", "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "Prometheus" - }, "expr": "rate(dialogporten_process_runtime_dotnet_gc_collections_count_total[5m])", - "legendFormat": "Gen {{generation}}", + "legendFormat": "Collections/sec", "refId": "A" + }, + { + "expr": "rate(dialogporten_process_runtime_dotnet_gc_duration_nanoseconds_total[5m])", + "legendFormat": "GC Duration/sec", + "refId": "B" } ] }, @@ -257,22 +264,19 @@ "type": "timeseries", "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "Prometheus" - }, "expr": "dialogporten_process_runtime_dotnet_thread_pool_queue_length", "legendFormat": "Queue Length", "refId": "A" }, { - "datasource": { - "type": "prometheus", - "uid": "Prometheus" - }, "expr": "dialogporten_process_runtime_dotnet_thread_pool_threads_count", "legendFormat": "Thread Count", "refId": "B" + }, + { + "expr": "rate(dialogporten_process_runtime_dotnet_thread_pool_completed_items_count_total[5m])", + "legendFormat": "Completed Items/sec", + "refId": "C" } ] }, @@ -352,20 +356,12 @@ "type": "timeseries", "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "Prometheus" - }, - "expr": "rate(dialogporten_process_runtime_dotnet_exceptions_count_total[$__rate_interval])", + "expr": "rate(dialogporten_process_runtime_dotnet_exceptions_count_total[5m])", "legendFormat": "Exceptions/sec", "refId": "A" }, { - "datasource": { - "type": "prometheus", - "uid": "Prometheus" - }, - "expr": "rate(dialogporten_process_runtime_dotnet_monitor_lock_contention_count_total[$__rate_interval])", + "expr": "rate(dialogporten_process_runtime_dotnet_monitor_lock_contention_count_total[5m])", "legendFormat": "Lock Contentions/sec", "refId": "B" } diff --git a/local-otel-configuration/grafana-datasources.yml b/local-otel-configuration/grafana-datasources.yml index 4139ccba6..0efe29c53 100644 --- a/local-otel-configuration/grafana-datasources.yml +++ b/local-otel-configuration/grafana-datasources.yml @@ -5,4 +5,11 @@ datasources: type: prometheus access: proxy url: http://prometheus:9090 - isDefault: true \ No newline at end of file + isDefault: true + + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + jsonData: + maxLines: 1000 \ No newline at end of file diff --git a/local-otel-configuration/loki-config.yaml b/local-otel-configuration/loki-config.yaml new file mode 100644 index 000000000..a9f289d68 --- /dev/null +++ b/local-otel-configuration/loki-config.yaml @@ -0,0 +1,45 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /tmp/loki + +compactor: + working_directory: /tmp/loki/compactor + compaction_interval: 10m + +ingester: + lifecycler: + address: 127.0.0.1 + ring: + kvstore: + store: inmemory + replication_factor: 1 + final_sleep: 0s + chunk_idle_period: 5m + chunk_retain_period: 30s + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + tsdb_shipper: + active_index_directory: /tmp/loki/tsdb-index + cache_location: /tmp/loki/tsdb-cache + cache_ttl: 24h + filesystem: + directory: /tmp/loki/chunks + +limits_config: + reject_old_samples: true + reject_old_samples_max_age: 168h + allow_structured_metadata: true diff --git a/local-otel-configuration/otel-collector-config.yaml b/local-otel-configuration/otel-collector-config.yaml index 97bc0c2b6..d5cba28c3 100644 --- a/local-otel-configuration/otel-collector-config.yaml +++ b/local-otel-configuration/otel-collector-config.yaml @@ -25,6 +25,8 @@ exporters: verbosity: detailed sampling_initial: 5 sampling_thereafter: 200 + otlphttp: + endpoint: "http://loki:3100/otlp" extensions: health_check: @@ -49,4 +51,4 @@ service: logs: receivers: [otlp] processors: [batch] - exporters: [debug] + exporters: [otlphttp, debug] diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs index 0ebda5a25..e13c358a8 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -30,7 +30,7 @@ public static class ClaimsPrincipalExtensions // TODO: This scope is also defined in WebAPI/GQL. Can this be fetched from a common auth lib? - // https://github.com/digdir/dialogporten/issues/647 + // https://github.com/altinn/dialogporten/issues/647 // This could be done for all claims/scopes/prefixes etc, there are duplicates public const string ServiceProviderScope = "digdir:dialogporten.serviceprovider"; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index e3c9d64e5..f67282da5 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -119,7 +119,7 @@ public async Task Handle(GetDialogQuery request, CancellationTo } // TODO: What if name lookup fails - // https://github.com/digdir/dialogporten/issues/387 + // https://github.com/altinn/dialogporten/issues/387 dialog.UpdateSeenAt( currentUserInformation.UserId.ExternalIdWithPrefix, currentUserInformation.UserId.Type, diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs index 9b9250ed6..8011a3dac 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs @@ -56,7 +56,7 @@ public async Task Handle(DeleteDialogCommand request, Cancel if (dialog.Deleted) { - // TODO: https://github.com/digdir/dialogporten/issues/1543 + // TODO: https://github.com/altinn/dialogporten/issues/1543 // When restoration is implemented, add a hint to the error message. return new EntityDeleted(request.Id); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs index 4b11eb5aa..afeac8281 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs @@ -94,7 +94,7 @@ public async Task Handle(UpdateDialogCommand request, Cancel if (dialog.Deleted) { - // TODO: https://github.com/digdir/dialogporten/issues/1543 + // TODO: https://github.com/altinn/dialogporten/issues/1543 // When restoration is implemented, add a hint to the error message. return new EntityDeleted(request.Id); } diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Extensions/OpenTelemetryExtensions.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Extensions/OpenTelemetryExtensions.cs new file mode 100644 index 000000000..1a8901b31 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Extensions/OpenTelemetryExtensions.cs @@ -0,0 +1,158 @@ +using Azure.Monitor.OpenTelemetry.Exporter; +using Npgsql; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System.Diagnostics; +using System.Globalization; +using Serilog; +using Serilog.Configuration; +using Serilog.Sinks.OpenTelemetry; + +namespace Digdir.Domain.Dialogporten.GraphQL.Common.Extensions; + +internal static class OpenTelemetryExtensions +{ + private const string OtelExporterOtlpEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT"; + private const string OtelExporterOtlpProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL"; + private const string DialogportenGraphQLSource = "Dialogporten.GraphQL"; + + public static IServiceCollection AddDialogportenTelemetry( + this IServiceCollection services, + IConfiguration configuration, + IHostEnvironment environment) + { + if (!Uri.IsWellFormedUriString(configuration[OtelExporterOtlpEndpoint], UriKind.Absolute)) + return services; + + var otlpProtocol = configuration[OtelExporterOtlpProtocol]?.ToLowerInvariant() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported protocol: {configuration[OtelExporterOtlpProtocol]}") + }; + + var endpoint = new Uri(configuration[OtelExporterOtlpEndpoint]!); + + return services.AddOpenTelemetry() + .ConfigureResource(resource => + { + resource.AddService( + serviceName: configuration["OTEL_SERVICE_NAME"] ?? environment.ApplicationName); + }) + .WithTracing(tracing => + { + if (environment.IsDevelopment()) + { + tracing.SetSampler(new AlwaysOnSampler()); + } + + tracing + .AddAspNetCoreInstrumentation(opts => + { + opts.RecordException = true; + opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health"); + }) + .AddHttpClientInstrumentation(o => + { + o.RecordException = true; + o.FilterHttpRequestMessage = _ => + { + var parentActivity = Activity.Current?.Parent; + if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http", StringComparison.Ordinal)) + { + return false; + } + return true; + }; + }) + .AddEntityFrameworkCoreInstrumentation() + .AddNpgsql() + .AddFusionCacheInstrumentation() + .AddSource(DialogportenGraphQLSource) + .AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint, "/v1/traces"); + options.Protocol = otlpProtocol; + }); + }) + .WithMetrics(metrics => + { + metrics.AddRuntimeInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + + var appInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + if (!string.IsNullOrEmpty(appInsightsConnectionString)) + { + metrics.AddAzureMonitorMetricExporter(options => + { + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + metrics.AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint, "/v1/metrics"); + options.Protocol = otlpProtocol; + }); + } + }) + .Services; + } + + public static LoggerConfiguration OpenTelemetryOrConsole(this LoggerSinkConfiguration writeTo, HostBuilderContext context) + { + var otelEndpoint = context.Configuration[OtelExporterOtlpEndpoint]; + var otelProtocol = context.Configuration[OtelExporterOtlpProtocol]; + + return otelEndpoint switch + { + null => + writeTo.Console(formatProvider: CultureInfo.InvariantCulture), + not null when Uri.IsWellFormedUriString(otelEndpoint, UriKind.Absolute) => + writeTo.OpenTelemetry(ConfigureOtlpSink(otelEndpoint, ParseOtlpProtocol(otelProtocol))), + _ => throw new InvalidOperationException($"Invalid otel endpoint: {otelEndpoint}") + }; + } + + public static LoggerConfiguration TryWriteToOpenTelemetry(this LoggerConfiguration config) + { + var otelEndpoint = Environment.GetEnvironmentVariable(OtelExporterOtlpEndpoint); + var otelProtocol = Environment.GetEnvironmentVariable(OtelExporterOtlpProtocol); + + if (otelEndpoint is null || !Uri.IsWellFormedUriString(otelEndpoint, UriKind.Absolute)) + { + return config; + } + + try + { + var protocol = ParseOtlpProtocol(otelProtocol); + return config.WriteTo.OpenTelemetry(ConfigureOtlpSink(otelEndpoint, protocol)); + } + catch (ArgumentException) + { + return config; + } + } + + private static OtlpProtocol ParseOtlpProtocol(string? protocol) + { + return protocol?.ToLowerInvariant() switch + { + "grpc" => OtlpProtocol.Grpc, + "http/protobuf" => OtlpProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported OTLP protocol: {protocol}") + }; + } + + private static Action ConfigureOtlpSink(string endpoint, OtlpProtocol protocol) => + options => + { + options.Endpoint = endpoint; + options.Protocol = protocol; + }; +} diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj b/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj index 5eddd36b8..388e25305 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj @@ -15,6 +15,15 @@ + + + + + + + + + diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs index 5808a8408..f106411af 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs @@ -10,28 +10,25 @@ using Digdir.Domain.Dialogporten.Application.Common.Extensions.OptionExtensions; using Digdir.Domain.Dialogporten.GraphQL; using Digdir.Library.Utils.AspNet; -using Microsoft.ApplicationInsights.Extensibility; using Serilog; using FluentValidation; using HotChocolate.AspNetCore; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; +using Digdir.Domain.Dialogporten.GraphQL.Common.Extensions; -const string DialogportenGraphQLSource = "Dialogporten.GraphQL"; - -var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); // Using two-stage initialization to catch startup errors. Log.Logger = new LoggerConfiguration() .MinimumLevel.Warning() - .Enrich.FromLogContext() .Enrich.WithEnvironmentName() + .Enrich.FromLogContext() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) - .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces) + .TryWriteToOpenTelemetry() .CreateBootstrapLogger(); try { - BuildAndRun(args, telemetryConfiguration); + BuildAndRun(args); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -43,22 +40,21 @@ Log.CloseAndFlush(); } -static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfiguration) +static void BuildAndRun(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Configuration + .AddAzureConfiguration(builder.Environment.EnvironmentName) + .AddLocalConfiguration(builder.Environment); + builder.Host.UseSerilog((context, services, configuration) => configuration .MinimumLevel.Warning() .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) - .Enrich.FromLogContext() .Enrich.WithEnvironmentName() - .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) - .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces)); - - builder.Configuration - .AddAzureConfiguration(builder.Environment.EnvironmentName) - .AddLocalConfiguration(builder.Environment); + .Enrich.FromLogContext() + .WriteTo.OpenTelemetryOrConsole(context)); builder.Services .AddOptions() @@ -68,16 +64,6 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura var thisAssembly = Assembly.GetExecutingAssembly(); - builder.ConfigureTelemetry((settings, configuration) => - { - settings.ServiceName = configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName; - settings.Endpoint = configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; - settings.Protocol = configuration["OTEL_EXPORTER_OTLP_PROTOCOL"]; - settings.AppInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; - settings.ResourceAttributes = configuration["OTEL_RESOURCE_ATTRIBUTES"]; - settings.TraceSources.Add(DialogportenGraphQLSource); - }); - builder.Services // Options setup .ConfigureOptions() @@ -130,7 +116,6 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .UseAuthentication() .UseAuthorization() .UseMiddleware() - .UseSerilogRequestLogging() .UseAzureConfiguration(); app.MapGraphQL() diff --git a/src/Digdir.Domain.Dialogporten.Service/Common/Extensions/OpenTelemetryExtensions.cs b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions/OpenTelemetryExtensions.cs new file mode 100644 index 000000000..d91128848 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Service/Common/Extensions/OpenTelemetryExtensions.cs @@ -0,0 +1,155 @@ +using Azure.Monitor.OpenTelemetry.Exporter; +using Npgsql; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System.Diagnostics; +using System.Globalization; +using Serilog; +using Serilog.Configuration; +using Serilog.Sinks.OpenTelemetry; + +namespace Digdir.Domain.Dialogporten.Service.Common.Extensions; + +internal static class OpenTelemetryExtensions +{ + private const string OtelExporterOtlpEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT"; + private const string OtelExporterOtlpProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL"; + + public static IServiceCollection AddDialogportenTelemetry( + this IServiceCollection services, + IConfiguration configuration, + IHostEnvironment environment) + { + if (!Uri.IsWellFormedUriString(configuration[OtelExporterOtlpEndpoint], UriKind.Absolute)) + return services; + + var otlpProtocol = configuration[OtelExporterOtlpProtocol]?.ToLowerInvariant() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported protocol: {configuration[OtelExporterOtlpProtocol]}") + }; + + var endpoint = new Uri(configuration[OtelExporterOtlpEndpoint]!); + + return services.AddOpenTelemetry() + .ConfigureResource(resource => + { + resource.AddService( + serviceName: configuration["OTEL_SERVICE_NAME"] ?? environment.ApplicationName); + }) + .WithTracing(tracing => + { + if (environment.IsDevelopment()) + { + tracing.SetSampler(new AlwaysOnSampler()); + } + + tracing + .AddAspNetCoreInstrumentation(opts => + { + opts.RecordException = true; + opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health"); + }) + .AddHttpClientInstrumentation(o => + { + o.RecordException = true; + o.FilterHttpRequestMessage = _ => + { + var parentActivity = Activity.Current?.Parent; + if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http", StringComparison.Ordinal)) + { + return false; + } + return true; + }; + }) + .AddEntityFrameworkCoreInstrumentation() + .AddNpgsql() + .AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint, "/v1/traces"); + options.Protocol = otlpProtocol; + }); + }) + .WithMetrics(metrics => + { + metrics.AddRuntimeInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + + var appInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + if (!string.IsNullOrEmpty(appInsightsConnectionString)) + { + metrics.AddAzureMonitorMetricExporter(options => + { + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + metrics.AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint, "/v1/metrics"); + options.Protocol = otlpProtocol; + }); + } + }) + .Services; + } + + public static LoggerConfiguration OpenTelemetryOrConsole(this LoggerSinkConfiguration writeTo, HostBuilderContext context) + { + var otelEndpoint = context.Configuration[OtelExporterOtlpEndpoint]; + var otelProtocol = context.Configuration[OtelExporterOtlpProtocol]; + + return otelEndpoint switch + { + null => + writeTo.Console(formatProvider: CultureInfo.InvariantCulture), + not null when Uri.IsWellFormedUriString(otelEndpoint, UriKind.Absolute) => + writeTo.OpenTelemetry(ConfigureOtlpSink(otelEndpoint, ParseOtlpProtocol(otelProtocol))), + _ => throw new InvalidOperationException($"Invalid otel endpoint: {otelEndpoint}") + }; + } + + public static LoggerConfiguration TryWriteToOpenTelemetry(this LoggerConfiguration config) + { + var otelEndpoint = Environment.GetEnvironmentVariable(OtelExporterOtlpEndpoint); + var otelProtocol = Environment.GetEnvironmentVariable(OtelExporterOtlpProtocol); + + if (otelEndpoint is null || !Uri.IsWellFormedUriString(otelEndpoint, UriKind.Absolute)) + { + return config; + } + + try + { + var protocol = ParseOtlpProtocol(otelProtocol); + return config.WriteTo.OpenTelemetry(ConfigureOtlpSink(otelEndpoint, protocol)); + } + catch (ArgumentException) + { + return config; + } + } + + private static OtlpProtocol ParseOtlpProtocol(string? protocol) + { + return protocol?.ToLowerInvariant() switch + { + "grpc" => OtlpProtocol.Grpc, + "http/protobuf" => OtlpProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported OTLP protocol: {protocol}") + }; + } + + private static Action ConfigureOtlpSink(string endpoint, OtlpProtocol protocol) => + options => + { + options.Endpoint = endpoint; + options.Protocol = protocol; + }; +} diff --git a/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj b/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj index a4b65ddb0..57eab9463 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj +++ b/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj @@ -4,6 +4,14 @@ + + + + + + + + diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index 28a1c3a39..addd46d71 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -2,7 +2,6 @@ using Digdir.Domain.Dialogporten.Application; using Digdir.Domain.Dialogporten.Infrastructure; using Digdir.Domain.Dialogporten.Application.Common.Extensions; -using Microsoft.ApplicationInsights.Extensibility; using Serilog; using Digdir.Domain.Dialogporten.Application.Externals.Presentation; using Digdir.Domain.Dialogporten.Service; @@ -10,20 +9,20 @@ using Digdir.Library.Utils.AspNet; using MassTransit; using Microsoft.Extensions.DependencyInjection.Extensions; +using Digdir.Domain.Dialogporten.Service.Common.Extensions; // Using two-stage initialization to catch startup errors. -var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Warning() - .Enrich.FromLogContext() .Enrich.WithEnvironmentName() + .Enrich.FromLogContext() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) - .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces) + .TryWriteToOpenTelemetry() .CreateBootstrapLogger(); try { - BuildAndRun(args, telemetryConfiguration); + BuildAndRun(args); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -35,30 +34,21 @@ Log.CloseAndFlush(); } -static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfiguration) +static void BuildAndRun(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Configuration + .AddAzureConfiguration(builder.Environment.EnvironmentName) + .AddLocalConfiguration(builder.Environment); + builder.Host.UseSerilog((context, services, configuration) => configuration .MinimumLevel.Warning() .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) - .Enrich.FromLogContext() .Enrich.WithEnvironmentName() - .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces)); - - builder.Configuration - .AddAzureConfiguration(builder.Environment.EnvironmentName) - .AddLocalConfiguration(builder.Environment); - - builder.ConfigureTelemetry((settings, configuration) => - { - settings.ServiceName = configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName; - settings.Endpoint = configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; - settings.Protocol = configuration["OTEL_EXPORTER_OTLP_PROTOCOL"]; - settings.AppInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; - settings.ResourceAttributes = configuration["OTEL_RESOURCE_ATTRIBUTES"]; - }); + .Enrich.FromLogContext() + .WriteTo.OpenTelemetryOrConsole(context)); builder.Services .AddAzureAppConfiguration() diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/OpenTelemetryExtensions.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/OpenTelemetryExtensions.cs new file mode 100644 index 000000000..a81c8bb54 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/OpenTelemetryExtensions.cs @@ -0,0 +1,156 @@ +using Azure.Monitor.OpenTelemetry.Exporter; +using Npgsql; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System.Diagnostics; +using System.Globalization; +using Serilog; +using Serilog.Configuration; +using Serilog.Sinks.OpenTelemetry; + +namespace Digdir.Domain.Dialogporten.WebApi.Common.Extensions; + +internal static class OpenTelemetryExtensions +{ + private const string OtelExporterOtlpEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT"; + private const string OtelExporterOtlpProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL"; + + public static IServiceCollection AddDialogportenTelemetry( + this IServiceCollection services, + IConfiguration configuration, + IHostEnvironment environment) + { + if (!Uri.IsWellFormedUriString(configuration[OtelExporterOtlpEndpoint], UriKind.Absolute)) + return services; + + var otlpProtocol = configuration[OtelExporterOtlpProtocol]?.ToLowerInvariant() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported protocol: {configuration[OtelExporterOtlpProtocol]}") + }; + + var endpoint = new Uri(configuration[OtelExporterOtlpEndpoint]!); + + return services.AddOpenTelemetry() + .ConfigureResource(resource => + { + resource.AddService( + serviceName: configuration["OTEL_SERVICE_NAME"] ?? environment.ApplicationName); + }) + .WithTracing(tracing => + { + if (environment.IsDevelopment()) + { + tracing.SetSampler(new AlwaysOnSampler()); + } + + tracing + .AddAspNetCoreInstrumentation(opts => + { + opts.RecordException = true; + opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health"); + }) + .AddHttpClientInstrumentation(o => + { + o.RecordException = true; + o.FilterHttpRequestMessage = _ => + { + var parentActivity = Activity.Current?.Parent; + if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http", StringComparison.Ordinal)) + { + return false; + } + return true; + }; + }) + .AddEntityFrameworkCoreInstrumentation() + .AddNpgsql() + .AddFusionCacheInstrumentation() + .AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint, "/v1/traces"); + options.Protocol = otlpProtocol; + }); + }) + .WithMetrics(metrics => + { + metrics.AddRuntimeInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + + var appInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + if (!string.IsNullOrEmpty(appInsightsConnectionString)) + { + metrics.AddAzureMonitorMetricExporter(options => + { + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + metrics.AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint, "/v1/metrics"); + options.Protocol = otlpProtocol; + }); + } + }) + .Services; + } + + public static LoggerConfiguration OpenTelemetryOrConsole(this LoggerSinkConfiguration writeTo, HostBuilderContext context) + { + var otelEndpoint = context.Configuration[OtelExporterOtlpEndpoint]; + var otelProtocol = context.Configuration[OtelExporterOtlpProtocol]; + + return otelEndpoint switch + { + null => + writeTo.Console(formatProvider: CultureInfo.InvariantCulture), + not null when Uri.IsWellFormedUriString(otelEndpoint, UriKind.Absolute) => + writeTo.OpenTelemetry(ConfigureOtlpSink(otelEndpoint, ParseOtlpProtocol(otelProtocol))), + _ => throw new InvalidOperationException($"Invalid otel endpoint: {otelEndpoint}") + }; + } + + public static LoggerConfiguration TryWriteToOpenTelemetry(this LoggerConfiguration config) + { + var otelEndpoint = Environment.GetEnvironmentVariable(OtelExporterOtlpEndpoint); + var otelProtocol = Environment.GetEnvironmentVariable(OtelExporterOtlpProtocol); + + if (otelEndpoint is null || !Uri.IsWellFormedUriString(otelEndpoint, UriKind.Absolute)) + { + return config; + } + + try + { + var protocol = ParseOtlpProtocol(otelProtocol); + return config.WriteTo.OpenTelemetry(ConfigureOtlpSink(otelEndpoint, protocol)); + } + catch (ArgumentException) + { + return config; + } + } + + private static OtlpProtocol ParseOtlpProtocol(string? protocol) + { + return protocol?.ToLowerInvariant() switch + { + "grpc" => OtlpProtocol.Grpc, + "http/protobuf" => OtlpProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported OTLP protocol: {protocol}") + }; + } + + private static Action ConfigureOtlpSink(string endpoint, OtlpProtocol protocol) => + options => + { + options.Endpoint = endpoint; + options.Protocol = protocol; + }; +} diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj b/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj index c360532b9..d06d3cd74 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj +++ b/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj @@ -13,11 +13,19 @@ + - + + + + + + + + diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs index 82f7efb89..a2a9850f6 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs @@ -14,7 +14,7 @@ public CreateDialogEndpointSummary() Description = """ The dialog is created with the given configuration. For more information see the documentation (link TBD). - For detailed information on validation rules, see [the source for CreateDialogCommandValidator](https://github.com/digdir/dialogporten/blob/main/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs) + For detailed information on validation rules, see [the source for CreateDialogCommandValidator](https://github.com/altinn/dialogporten/blob/main/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs) """; ResponseExamples[StatusCodes.Status201Created] = "018bb8e5-d9d0-7434-8ec5-569a6c8e01fc"; diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs index 2e91d65ae..5521566ce 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs @@ -7,12 +7,12 @@ using Digdir.Domain.Dialogporten.Application.Common.Extensions; using Digdir.Domain.Dialogporten.Application.Common.Extensions.OptionExtensions; using Digdir.Domain.Dialogporten.Application.Externals.Presentation; -using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; using Digdir.Domain.Dialogporten.Infrastructure; using Digdir.Domain.Dialogporten.WebApi; using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authentication; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; +using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; using Digdir.Domain.Dialogporten.WebApi.Common.Json; using Digdir.Domain.Dialogporten.WebApi.Common.Swagger; using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Patch; @@ -20,25 +20,23 @@ using FastEndpoints; using FastEndpoints.Swagger; using FluentValidation; -using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; using NSwag; using Serilog; -using Microsoft.Extensions.Options; // Using two-stage initialization to catch startup errors. -var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Warning() .Enrich.WithEnvironmentName() .Enrich.FromLogContext() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) - .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces) + .TryWriteToOpenTelemetry() .CreateBootstrapLogger(); try { - BuildAndRun(args, telemetryConfiguration); + BuildAndRun(args); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -50,7 +48,7 @@ Log.CloseAndFlush(); } -static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfiguration) +static void BuildAndRun(string[] args) { var builder = WebApplication.CreateBuilder(args); @@ -59,17 +57,17 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura kestrelOptions.Limits.MaxRequestBodySize = Constants.MaxRequestBodySize; }); + builder.Configuration + .AddAzureConfiguration(builder.Environment.EnvironmentName) + .AddLocalConfiguration(builder.Environment); + builder.Host.UseSerilog((context, services, configuration) => configuration .MinimumLevel.Warning() .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.WithEnvironmentName() .Enrich.FromLogContext() - .WriteTo.ApplicationInsights(telemetryConfiguration, TelemetryConverter.Traces)); - - builder.Configuration - .AddAzureConfiguration(builder.Environment.EnvironmentName) - .AddLocalConfiguration(builder.Environment); + .WriteTo.OpenTelemetryOrConsole(context)); builder.Services .AddOptions() @@ -79,16 +77,8 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura var thisAssembly = Assembly.GetExecutingAssembly(); - builder.ConfigureTelemetry((settings, configuration) => - { - settings.ServiceName = configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName; - settings.Endpoint = configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; - settings.Protocol = configuration["OTEL_EXPORTER_OTLP_PROTOCOL"]; - settings.AppInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; - settings.ResourceAttributes = configuration["OTEL_RESOURCE_ATTRIBUTES"]; - }); - builder.Services + .AddDialogportenTelemetry(builder.Configuration, builder.Environment) // Options setup .ConfigureOptions() @@ -158,11 +148,7 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura var app = builder.Build(); - app.MapAspNetHealthChecks() - .MapControllers(); - app.UseHttpsRedirection() - .UseSerilogRequestLogging() .UseDefaultExceptionHandler() .UseJwtSchemeSelector() .UseAuthentication() diff --git a/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs b/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs index c1388ec0d..ddb77607a 100644 --- a/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs +++ b/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs @@ -2,18 +2,8 @@ using HealthChecks.UI.Client; 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 Npgsql; -using OpenTelemetry.Trace; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry; -using OpenTelemetry.Exporter; -using System.Diagnostics; -using Azure.Monitor.OpenTelemetry.Exporter; namespace Digdir.Library.Utils.AspNet; @@ -50,118 +40,4 @@ private static WebApplication MapHealthCheckEndpoint(this WebApplication app, st app.MapHealthChecks(path, new HealthCheckOptions { Predicate = predicate, ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); return app; } - - public static WebApplicationBuilder ConfigureTelemetry( - this WebApplicationBuilder builder, - Action? configure = null) - { - var settings = new TelemetrySettings(); - configure?.Invoke(settings, builder.Configuration); - - Console.WriteLine($"[OpenTelemetry] Configuring telemetry for service: {settings.ServiceName}"); - - var telemetryBuilder = builder.Services.AddOpenTelemetry() - .ConfigureResource(resource => - { - var resourceBuilder = resource.AddService(serviceName: settings.ServiceName ?? builder.Environment.ApplicationName); - - var resourceAttributes = settings.ResourceAttributes; - if (string.IsNullOrEmpty(resourceAttributes)) return; - - try - { - var attributes = resourceAttributes - .Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(pair => pair.Split('=', 2)) - .Where(parts => parts.Length == 2 && !string.IsNullOrEmpty(parts[0])) - .Select(parts => new KeyValuePair(parts[0].Trim(), parts[1].Trim())); - - foreach (var attribute in attributes) - { - resourceBuilder.AddAttributes([attribute]); - } - } - catch (Exception ex) - { - throw new InvalidOperationException( - "Failed to parse OTEL_RESOURCE_ATTRIBUTES. Expected format: key1=value1,key2=value2", - ex - ); - } - }); - - if (!string.IsNullOrEmpty(settings.Endpoint) && !string.IsNullOrEmpty(settings.Protocol)) - { - Console.WriteLine($"[OpenTelemetry] Using endpoint: {settings.Endpoint}"); - Console.WriteLine($"[OpenTelemetry] Using protocol: {settings.Protocol}"); - - var otlpProtocol = settings.Protocol.ToLowerInvariant() switch - { - "grpc" => OtlpExportProtocol.Grpc, - "http/protobuf" => OtlpExportProtocol.HttpProtobuf, - "http" => OtlpExportProtocol.HttpProtobuf, - _ => throw new ArgumentException($"Unsupported protocol: {settings.Protocol}") - }; - - telemetryBuilder.UseOtlpExporter(otlpProtocol, new Uri(settings.Endpoint)); - - telemetryBuilder - .WithTracing(tracing => - { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - - foreach (var source in settings.TraceSources) - { - tracing.AddSource(source); - } - - tracing - .AddAspNetCoreInstrumentation(opts => - { - opts.RecordException = true; - opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health"); - }) - .AddHttpClientInstrumentation(o => - { - o.RecordException = true; - o.FilterHttpRequestMessage = _ => - { - var parentActivity = Activity.Current?.Parent; - if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http", StringComparison.Ordinal)) - { - return false; - } - return true; - }; - }) - .AddEntityFrameworkCoreInstrumentation() - .AddNpgsql() - .AddFusionCacheInstrumentation(); - }); - - telemetryBuilder.WithMetrics(metrics => - { - metrics.AddRuntimeInstrumentation() - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation(); - - if (!string.IsNullOrEmpty(settings.AppInsightsConnectionString)) - { - metrics.AddAzureMonitorMetricExporter(options => - { - options.ConnectionString = settings.AppInsightsConnectionString; - }); - } - }); - } - else - { - Console.WriteLine("[OpenTelemetry] OTLP exporter not configured - skipping"); - } - - return builder; - } } diff --git a/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj b/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj index 36a45f2a5..b5343c643 100644 --- a/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj +++ b/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj @@ -7,19 +7,12 @@ - - - - - - - diff --git a/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs b/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs index 4cbd05a9d..cb25a140e 100644 --- a/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs +++ b/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs @@ -257,7 +257,7 @@ public static List GenerateFakeDialogTransmissions(int? count = public static List GenerateFakeDialogActivities(int? count = null, DialogActivityType.Values? type = null) { // Temporarily removing the ActivityType TransmissionOpened from the list of possible types for random picking. - // Going to have a look at re-writing the generator https://github.com/digdir/dialogporten/issues/1123 + // Going to have a look at re-writing the generator https://github.com/altinn/dialogporten/issues/1123 var activityTypes = Enum.GetValues() .Where(x => x != DialogActivityType.Values.TransmissionOpened).ToList(); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj index 9accc8b47..a781f24c0 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj b/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj index ec2451436..f770a7f96 100644 --- a/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj @@ -13,7 +13,7 @@ - + all diff --git a/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj b/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj index f6f7ba2ea..7313d9181 100644 --- a/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj @@ -8,7 +8,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Features/V1/SwaggerSnapshotTests.cs b/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Features/V1/SwaggerSnapshotTests.cs index 187a1269d..32b0f33e5 100644 --- a/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Features/V1/SwaggerSnapshotTests.cs +++ b/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Features/V1/SwaggerSnapshotTests.cs @@ -36,7 +36,7 @@ public async Task FailIfSwaggerSnapshotDoesNotMatch() // Running client.GetAsync("/swagger/v1/swagger.json"); on Windows and Mac will produce // different ordering of the results (although the content is the same). So we force an // alphabetical ordering of the properties to make the test deterministic. - // Ref: https://github.com/digdir/dialogporten/issues/996 + // Ref: https://github.com/altinn/dialogporten/issues/996 var orderedSwagger = SortJson(newSwagger); // Assert diff --git a/tests/k6/tests/enduser/performance/README.md b/tests/k6/tests/enduser/performance/README.md index 0a88e425a..c1af12afd 100644 --- a/tests/k6/tests/enduser/performance/README.md +++ b/tests/k6/tests/enduser/performance/README.md @@ -44,7 +44,7 @@ k6 run enduser-search.js -e API_VERSION=v1 \ ### From GitHub Actions To run the performance test using GitHub Actions, follow these steps: -1. Go to the [GitHub Actions](https://github.com/digdir/dialogporten/actions/workflows/dispatch-k6-performance.yml) page. +1. Go to the [GitHub Actions](https://github.com/altinn/dialogporten/actions/workflows/dispatch-k6-performance.yml) page. 2. Select "Run workflow" and fill in the required parameters. 3. Tag the performance test with a descriptive name. diff --git a/tests/k6/tests/graphql/performance/README.md b/tests/k6/tests/graphql/performance/README.md index 0faa92506..7410616fe 100644 --- a/tests/k6/tests/graphql/performance/README.md +++ b/tests/k6/tests/graphql/performance/README.md @@ -30,7 +30,7 @@ k6 run graphql-search.js -e API_VERSION=v1 \ 4. Refer to the k6 documentation for more information on usage. ### From GitHub Actions To run the performance test using GitHub Actions, follow these steps: -1. Go to the [GitHub Actions](https://github.com/digdir/dialogporten/actions/workflows/dispatch-k6-performance.yml) page. +1. Go to the [GitHub Actions](https://github.com/altinn/dialogporten/actions/workflows/dispatch-k6-performance.yml) page. 2. Select "Run workflow" and fill in the required parameters. 3. Tag the performance test with a descriptive name. diff --git a/tests/k6/tests/serviceowner/performance/README.md b/tests/k6/tests/serviceowner/performance/README.md index 4155dfb6f..778a4f504 100644 --- a/tests/k6/tests/serviceowner/performance/README.md +++ b/tests/k6/tests/serviceowner/performance/README.md @@ -37,7 +37,7 @@ k6 run -e API_VERSION=v1 \ #### From GitHub Actions To run the performance test using GitHub Actions, follow these steps: -1. Go to the [GitHub Actions](https://github.com/digdir/dialogporten/actions/workflows/dispatch-k6-performance.yml) page. +1. Go to the [GitHub Actions](https://github.com/altinn/dialogporten/actions/workflows/dispatch-k6-performance.yml) page. 2. Select "Run workflow" and fill in the required parameters. 3. Tag the performance test with a descriptive name.