diff --git a/.docker/grafana-datasources.yaml b/.docker/grafana-datasources.yaml
new file mode 100644
index 0000000..a23ad46
--- /dev/null
+++ b/.docker/grafana-datasources.yaml
@@ -0,0 +1,39 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ type: prometheus
+ access: proxy
+ orgId: 1
+ url: http://prometheus:9090
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ - name: Tempo
+ type: tempo
+ access: proxy
+ orgId: 1
+ url: http://tempo:3200
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ apiVersion: 1
+ uid: tempo
+ - name: Loki
+ type: loki
+ access: proxy
+ orgId: 1
+ url: http://loki:3100
+ basicAuth: false
+ isDefault: true
+ version: 1
+ editable: false
+ apiVersion: 1
+ jsonData:
+ derivedFields:
+ - datasourceUid: tempo
+ matcherRegex: (?:"traceid"):"(\w+)"
+ name: TraceID
+ url: $${__value.raw}
diff --git a/.docker/opentelemetry-collector.yml b/.docker/opentelemetry-collector.yml
new file mode 100644
index 0000000..04a8378
--- /dev/null
+++ b/.docker/opentelemetry-collector.yml
@@ -0,0 +1,42 @@
+receivers:
+ otlp:
+ protocols:
+ grpc:
+
+exporters:
+ prometheus:
+ endpoint: "0.0.0.0:8889"
+ logging:
+ zipkin:
+ endpoint: "http://zipkin:9411/api/v2/spans"
+ format: proto
+ file:
+ path: /etc/output/logs.json
+ otlp:
+ endpoint: tempo:4317
+ tls:
+ insecure: true
+ loki:
+ endpoint: "http://loki:3100/loki/api/v1/push"
+ format: json
+ labels:
+ resource:
+ service.name: "service_name"
+ service.instance.id: "service_instance_id"
+
+processors:
+ batch:
+
+service:
+ pipelines:
+ logs:
+ receivers: [otlp]
+ exporters: [logging, file, loki]
+ metrics:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [logging, prometheus]
+ traces:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [logging, zipkin, otlp]
diff --git a/.docker/prometheus.yml b/.docker/prometheus.yml
new file mode 100644
index 0000000..a847754
--- /dev/null
+++ b/.docker/prometheus.yml
@@ -0,0 +1,6 @@
+scrape_configs:
+ - job_name: 'otel-collector'
+ scrape_interval: 10s
+ static_configs:
+ - targets: ['otel-collector:8889']
+ - targets: ['otel-collector:8888']
diff --git a/.docker/tempo.yaml b/.docker/tempo.yaml
new file mode 100644
index 0000000..c12aa40
--- /dev/null
+++ b/.docker/tempo.yaml
@@ -0,0 +1,15 @@
+server:
+ http_listen_port: 3200
+
+distributor:
+ receivers:
+ otlp:
+ protocols:
+ http:
+ grpc:
+
+storage:
+ trace:
+ backend: local
+ local:
+ path: /tmp/tempo/blocks
diff --git a/.gitignore b/.gitignore
index a559535..d2f65af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -358,3 +358,6 @@ Local.md
# GitLab runner files
/builds/
/.gitlab/runner/local/
+
+# Local build and log files
+/output/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 43aa9c6..2b68f3e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -26,6 +26,7 @@ workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "external_pull_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+ - if: '$CI_PIPELINE_SOURCE == "web"'
#.dotnet_build:
build:
@@ -48,16 +49,20 @@ build:
test:
stage: test
variables:
- ServiceNow__RestApi__BaseUrl: http://localhost:3000/api/now
- ServiceNow__RestApi__Username: dummy
- ServiceNow__RestApi__Password: dummy
COBERTURA_REPORT_FILEPATH: ./test/*/TestResults/*/coverage.cobertura.xml
REPORTGENERATOR_OPTIONS: -targetdir:sonarqubecoverage -reporttypes:SonarQube
SONAR_EXTRA_PARAMETERS: /d:sonar.cpd.exclusions=**/*Generated*.cs /d:sonar.coverageReportPaths=./sonarqubecoverage/SonarQube.xml
+ Application__IsSecuredByAzureAd: "false"
+ OpenTelemetry__Service: SampleServiceNowRestClient
+ OpenTelemetry__Metrics__Meter: SampleServiceNowRestClientMetrics
+ OpenTelemetry__Tracing__Source: SampleServiceNowRestClientTracing
+ ServiceNow__RestApi__BaseUrl: "http://localhost:3000/api/now"
+ ServiceNow__RestApi__Password: dummy
+ ServiceNow__RestApi__Username: dummy
ServiceNow__SqlServer__DataSource: mssql
- ServiceNow__SqlServer__UserId: SA
- ServiceNow__SqlServer__Password: $SA_PASSWORD
ServiceNow__SqlServer__InitialCatalog: TestDB
+ ServiceNow__SqlServer__Password: $SA_PASSWORD
+ ServiceNow__SqlServer__UserId: SA
before_script:
# updates system
- apt-get update
@@ -105,6 +110,7 @@ test:
only:
- external_pull_requests
- main
+ - web
# ref. https://docs.microsoft.com/en-us/nuget/nuget-org/publish-a-package
#.dotnet_pack:
diff --git a/Directory.Build.props b/Directory.Build.props
index bf566de..0df9578 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,7 +5,7 @@
- 1.1.0
+ 1.2.0
diff --git a/README.md b/README.md
index 30f388f..2abb71a 100644
--- a/README.md
+++ b/README.md
@@ -5,17 +5,22 @@
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=rabbids-incubator_servicenow-dotnet-client&metric=coverage)](https://sonarcloud.io/summary/new_code?id=rabbids-incubator_servicenow-dotnet-client)
[![Nuget](https://img.shields.io/nuget/v/RabbidsIncubator.ServiceNowClient.Application.svg)](https://www.nuget.org/packages/RabbidsIncubator.ServiceNowClient.Application)
-This is the codebase of .NET components (API & libraries) to simplify the integration with [ServiceNow](https://www.servicenow.com/),
-from any system (Linux, MacOS, Windows).
+.NET components (API & libraries) to simplify the integration with [ServiceNow](https://www.servicenow.com/), from any system (Linux, MacOS, Windows).
-## Quick start
+## Getting started
-* [Getting Started](./docs/getting-started.md) page to learn about how to use the components
+* [Quickstart](./docs/quickstart.md) to get an application running in 5 minutes
-* [Contribute](./docs/contribute.md) page to work locally with the sources and improve them
+* [Contribution](./docs/contribution.md) to run locally the solution and push code changes
## Going further
-* [GitLab](./docs/gitlab.md) page to understand the automation done on this codebase
+* [Authentication](./docs/authentication.md) to secure the application
-* [ServiceNow](./docs/servicenow.md) page for resources on the SaaS solution
+* [Automation](./docs/automation.md) to know about the Continuous Integration and Delivery pipeline
+
+* [Dependencies](./docs/dependencies.md) to view the dependencies (in particular the link with ServiceNow)
+
+* [Design](./docs/design.md) to understand the software design of the solution
+
+* [Observability](./docs/automation.md) to monitor and measure the application
diff --git a/RabbidsIncubator.ServiceNowClient.sln b/RabbidsIncubator.ServiceNowClient.sln
index 962bc97..7d3a0b3 100644
--- a/RabbidsIncubator.ServiceNowClient.sln
+++ b/RabbidsIncubator.ServiceNowClient.sln
@@ -32,10 +32,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Applicat
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{D314857E-9EE4-4947-AD1A-0949B04E28CA}"
ProjectSection(SolutionItems) = preProject
- docs\contribute.md = docs\contribute.md
- docs\getting-started.md = docs\getting-started.md
- docs\gitlab.md = docs\gitlab.md
- docs\servicenow.md = docs\servicenow.md
+ docs\authentication.md = docs\authentication.md
+ docs\automation.md = docs\automation.md
+ docs\contribution.md = docs\contribution.md
+ docs\dependencies.md = docs\dependencies.md
+ docs\design.md = docs\design.md
+ docs\observability.md = docs\observability.md
+ docs\quickstart.md = docs\quickstart.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.InMemory", "src\Infrastructure.InMemory\Infrastructure.InMemory.csproj", "{A01619B4-8F70-417D-B4BD-1651D627770E}"
@@ -62,6 +65,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mocks", "mocks", "{B338D8C5
mocks\servicenow.json = mocks\servicenow.json
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{8B94AA54-5F8B-463E-9CA0-D7E75B6F60AE}"
+ ProjectSection(SolutionItems) = preProject
+ docker-compose.yml = docker-compose.yml
+ .docker\grafana-datasources.yaml = .docker\grafana-datasources.yaml
+ .docker\opentelemetry-collector.yml = .docker\opentelemetry-collector.yml
+ .docker\prometheus.yml = .docker\prometheus.yml
+ .docker\tempo.yaml = .docker\tempo.yaml
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +148,7 @@ Global
{84A01CD5-1282-4114-A519-6F4C3625CE60} = {B952F0A1-0DB3-4D9F-BD49-C3DB6389FCB4}
{EA2379E0-3613-4EED-9238-92E6D48C5A62} = {03243F43-0975-4776-A5D3-9BA7141293DA}
{B338D8C5-A1F8-4EAD-97D4-8E23624372BB} = {03243F43-0975-4776-A5D3-9BA7141293DA}
+ {8B94AA54-5F8B-463E-9CA0-D7E75B6F60AE} = {03243F43-0975-4776-A5D3-9BA7141293DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CF38BCED-2352-4EDA-A4D5-1462CCD9C659}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..e539291
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,62 @@
+version: "3.9"
+services:
+ zipkin:
+ image: openzipkin/zipkin:latest
+ container_name: zipkin
+ ports:
+ - "9411:9411"
+
+ otel-collector:
+ image: otel/opentelemetry-collector-contrib:latest
+ container_name: otel-collector
+ command: ["--config=/etc/otel-collector.yml"]
+ volumes:
+ - ./.docker/opentelemetry-collector.yml:/etc/otel-collector.yml
+ - ./output:/etc/output:rw
+ ports:
+ - "8888:8888" # prometheus metrics
+ - "8889:8889" # prometheus exporter
+ - "4317:4317" # oltp grpc receiver
+ depends_on:
+ - zipkin
+ - grafana
+
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: prometheus
+ volumes:
+ - ./.docker/prometheus.yml:/etc/prometheus/prometheus.yml
+ ports:
+ - "9090:9090"
+
+ tempo:
+ image: grafana/tempo:latest
+ container_name: tempo
+ command: [ "-config.file=/etc/tempo.yaml" ]
+ volumes:
+ - ./.docker/tempo.yaml:/etc/tempo.yaml
+ ports:
+ - "3200:3200"
+
+ loki:
+ image: grafana/loki:2.4.2
+ container_name: loki
+ command: [ "-config.file=/etc/loki/local-config.yaml" ]
+ ports:
+ - "3100:3100"
+
+ grafana:
+ image: grafana/grafana:latest
+ container_name: grafana
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./.docker/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
+ environment:
+ - GF_AUTH_ANONYMOUS_ENABLED=true
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
+ - GF_AUTH_DISABLE_LOGIN_FORM=true
+ depends_on:
+ - prometheus
+ - loki
+ - tempo
diff --git a/docs/authentication.md b/docs/authentication.md
new file mode 100644
index 0000000..6d285fa
--- /dev/null
+++ b/docs/authentication.md
@@ -0,0 +1,26 @@
+# Authentication
+
+## How to secure an ASP.NET application with Azure AD
+
+* In [Azure Portal](https://portal.azure.com/), in "Azure Active Directory > Application registrations",
+select "New registration"
+ * Only the same is mandatory
+ * Once created, the application is displayed
+ * Save the values of "Application (client) ID", "Directory (tenant) ID"
+* Update the application
+ * "Manifest": manually edit the content (`accessTokenAcceptedVersion` and `allowPublicClient` are null by default)
+
+ ```json
+ {
+ "accessTokenAcceptedVersion": 2,
+ "allowPublicClient": true,
+ }
+ ```
+
+ * "Certificates & secrets": in "Client Secrets", add a new secret and save the secret value
+ * "Api permissions": do "Grant admin consent for Default Directory" (Microsoft Graph > User.Read has been added by default)
+ * "Expose an API": set the application ID URI, "api://" is the default and correct choice
+ * "Expose an API": add a scope, for example "access_as_user" with "Admins and users" for the consent option
+
+* References:
+ * [Scenario: Protected web API](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-overview)
diff --git a/docs/gitlab.md b/docs/automation.md
similarity index 98%
rename from docs/gitlab.md
rename to docs/automation.md
index 2ed3126..c4c3c54 100644
--- a/docs/gitlab.md
+++ b/docs/automation.md
@@ -1,4 +1,4 @@
-# GitLab configuration guide
+# Automation
[GitLab](https://gitlab.com/) is used to run the CI (Continuous Integration) pipeline, which is defined in `.gitlab-ci.yml` file.
diff --git a/docs/contribute.md b/docs/contribution.md
similarity index 89%
rename from docs/contribute.md
rename to docs/contribution.md
index edc60c0..dc464f6 100644
--- a/docs/contribute.md
+++ b/docs/contribution.md
@@ -1,4 +1,4 @@
-# Contribution guide
+# Contribution
## Requirements
@@ -55,6 +55,12 @@ dotnet build
"IsSwaggerEnabled": true,
"IsHttpsEnforced": false
},
+ "AzureAd": {
+ "Domain": "xxx.onmicrosoft.com",
+ "TenantId": "xxx",
+ "ClientId": "xxx",
+ "ClientSecret": "xxx"
+ },
"Logging": {
"LogLevel": {
"RabbidsIncubator": "Debug",
@@ -188,7 +194,7 @@ SA_PASSWORD='s0m3Str0ng!P@ssw0rd'
# runs SQL Server in a container (can be accessed with localhost or 127.0.0.1 as Data Source)
docker pull mcr.microsoft.com/mssql/server:2019-latest
-docker run --rm --name mssql --hostname $MSSQL_HOST \
+docker run --name mssql --hostname $MSSQL_HOST \
-e "ACCEPT_EULA=Y" -e "SA_PASSWORD=$SA_PASSWORD" -p 1433:1433 \
-d mcr.microsoft.com/mssql/server:2019-latest
@@ -198,6 +204,29 @@ docker exec -it mssql bash
# initializes database with data
docker cp $PWD/scripts/mssql/db-init.sql mssql:/home/db-init.sql
-docker exec mssql ls "/home"
+#docker exec mssql ls "/home"
docker exec mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P $SA_PASSWORD -i /home/db-init.sql
```
+
+### Observability
+
+* Start containers (Prometheus, Zipkin and OpenTelemetry collector)
+
+```bash
+docker-compose up
+```
+
+* Open web UIs:
+ * [Zipkin](http://localhost:9411/zipkin/)
+ * [Prometheus](http://localhost:9090/graph)
+ * [Grafana](http://localhost:3000/)
+
+* Remove containers
+
+```bash
+docker-compose up
+```
+
+* Warnings:
+ * If you are running the containers on WSL, when the Linux VM memory is full you will face strange behaviors.
+Do not hesitate to restart regularly WSL: `wsl --shutdown`.
diff --git a/docs/servicenow.md b/docs/dependencies.md
similarity index 97%
rename from docs/servicenow.md
rename to docs/dependencies.md
index 21cded7..4d6f56c 100644
--- a/docs/servicenow.md
+++ b/docs/dependencies.md
@@ -1,4 +1,6 @@
-# ServiceNow resources
+# Dependencies resources
+
+### ServiceNow
## General
diff --git a/docs/dotnet.md b/docs/design.md
similarity index 53%
rename from docs/dotnet.md
rename to docs/design.md
index 955f4f1..dd52190 100644
--- a/docs/dotnet.md
+++ b/docs/design.md
@@ -1,4 +1,6 @@
-# .NET
+# Design
+
+This codebase uses .NET framework and C# language. All technologies are free, open source and cross-platform.
## Code generation
diff --git a/docs/observability.md b/docs/observability.md
new file mode 100644
index 0000000..9299966
--- /dev/null
+++ b/docs/observability.md
@@ -0,0 +1,27 @@
+# Observability
+
+This solution uses [OpenTelemetry](https://opentelemetry.io/) to send logs, traces and metrics
+in a vendor-neutral, robust and standardized way.
+
+## OpenTelemetry
+
+* Blog articles
+ * [Monitoring a .NET application using OpenTelemetry](
+https://www.meziantou.net/monitoring-a-dotnet-application-using-opentelemetry.htm), by Gérald Barré - November 15, 2021
+ * [OpenTelemetry in .NET](
+https://rafaelldi.blog/posts/open-telemetry-in-dotnet/) by Rival Abdrakhmanov - January 14, 2022
+ * [Optimally Configuring Open Telemetry Tracing for ASP.NET Core](
+https://rehansaeed.com/optimally-configuring-open-telemetry-tracing-for-asp-net-core/) by Muhammad Rehan Saeed - February 3, 2022
+* Documentation
+ * [.NET / Collect metrics](https://docs.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection)
+ * [OpenTelemetry / Instrumentation / .NET](https://opentelemetry.io/docs/instrumentation/net/)
+* Projects
+ * [ASP.NET Core Instrumentation for OpenTelemetry](
+https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.AspNetCore)
+ * Current metrics: `http.server.duration` (histogram)
+ * [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector)
+ * [OpenTelemetry Collector Contrib](https://github.com/open-telemetry/opentelemetry-collector-contrib)
+* Limitation
+ * .NET gRPC: [#3023](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3023)
+* Issue
+ * OpenTelemetry .NET RC9 not working with metrics: [#3078](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3078)
diff --git a/docs/getting-started.md b/docs/quickstart.md
similarity index 80%
rename from docs/getting-started.md
rename to docs/quickstart.md
index dbcab1a..dd14e09 100644
--- a/docs/getting-started.md
+++ b/docs/quickstart.md
@@ -1,4 +1,4 @@
-# Getting Started procedure
+# Quickstart
## Requirements
@@ -83,6 +83,22 @@ rm src/WebApi/appsettings.json
cat > src/WebApi/appsettings.json < src/WebApi/appsettings.json < src/WebApi/appsettings.Development.json < logger,
IConfigurationItemRelationshipRepository configurationItemRelationshipRepository)
+ : base(logger)
{
- _logger = logger;
_configurationItemRelationshipRepository = configurationItemRelationshipRepository;
}
@@ -23,7 +21,7 @@ public ConfigurationItemRelationshipController(
public async Task> Get()
{
var items = await _configurationItemRelationshipRepository.FindAllAsync();
- _logger.LogDebug("Number of items found: {itemsCount}", items.Count);
+ ReportListCount(items.Count);
return items;
}
}
diff --git a/samples/WebApiSample/Controllers/SwitchController.cs b/samples/WebApiSample/Controllers/SwitchController.cs
index f039e66..cf3cb91 100644
--- a/samples/WebApiSample/Controllers/SwitchController.cs
+++ b/samples/WebApiSample/Controllers/SwitchController.cs
@@ -6,15 +6,13 @@ namespace RabbidsIncubator.Samples.ServiceNowWebApiSample.Controllers
{
[ApiController]
[Route("switches")]
- public class SwitchController : ControllerBase
+ public class SwitchController : RabbidsIncubator.ServiceNowClient.Application.Mvc.ControllerBase
{
- private readonly ILogger _logger;
-
private readonly ISwitchRepository _switchRepository;
public SwitchController(ILogger logger, ISwitchRepository switchRepository)
+ : base(logger)
{
- _logger = logger;
_switchRepository = switchRepository;
}
@@ -22,7 +20,7 @@ public SwitchController(ILogger logger, ISwitchRepository swit
public async Task> Get([FromQuery] SwitchModel model, int? startIndex, int? limit)
{
var items = await _switchRepository.FindAllAsync(new QueryModel(model, startIndex, limit));
- _logger.LogDebug("Number of items found: {itemsCount}", items.Count);
+ ReportListCount(items.Count);
return items;
}
}
diff --git a/samples/WebApiSample/Infrastructure/ConfigurationItemRelationshipRepository.cs b/samples/WebApiSample/Infrastructure/ConfigurationItemRelationshipRepository.cs
index 842f054..23d5102 100644
--- a/samples/WebApiSample/Infrastructure/ConfigurationItemRelationshipRepository.cs
+++ b/samples/WebApiSample/Infrastructure/ConfigurationItemRelationshipRepository.cs
@@ -1,5 +1,6 @@
using AutoMapper;
using RabbidsIncubator.Samples.ServiceNowWebApiSample.Domain;
+using RabbidsIncubator.ServiceNowClient.Domain.Diagnostics;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.Dto;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.Repositories;
@@ -12,8 +13,9 @@ public ConfigurationItemRelationshipRepository(
ILogger logger,
IHttpClientFactory httpClientFactory,
IMapper mapper,
- ServiceNowRestClientConfiguration restApiConfiguration)
- : base(logger, httpClientFactory, mapper, restApiConfiguration)
+ ServiceNowRestClientConfiguration restApiConfiguration,
+ IMetricsContext metricsContext)
+ : base(logger, httpClientFactory, mapper, restApiConfiguration, metricsContext)
{
}
diff --git a/samples/WebApiSample/Infrastructure/SwitchRepository.cs b/samples/WebApiSample/Infrastructure/SwitchRepository.cs
index ece6a4a..2e81777 100644
--- a/samples/WebApiSample/Infrastructure/SwitchRepository.cs
+++ b/samples/WebApiSample/Infrastructure/SwitchRepository.cs
@@ -1,5 +1,6 @@
using AutoMapper;
using RabbidsIncubator.Samples.ServiceNowWebApiSample.Domain;
+using RabbidsIncubator.ServiceNowClient.Domain.Diagnostics;
using RabbidsIncubator.ServiceNowClient.Domain.Models;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.Repositories;
@@ -12,8 +13,9 @@ public SwitchRepository(
ILogger logger,
IHttpClientFactory httpClientFactory,
IMapper mapper,
- ServiceNowRestClientConfiguration restApiConfiguration)
- : base(logger, httpClientFactory, mapper, restApiConfiguration)
+ ServiceNowRestClientConfiguration restApiConfiguration,
+ IMetricsContext metricsContext)
+ : base(logger, httpClientFactory, mapper, restApiConfiguration, metricsContext)
{
}
diff --git a/samples/WebApiSample/Program.cs b/samples/WebApiSample/Program.cs
index 058d64d..d2c6c04 100644
--- a/samples/WebApiSample/Program.cs
+++ b/samples/WebApiSample/Program.cs
@@ -1,5 +1,5 @@
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddDefaultServices(builder.Configuration, new GeneratedServiceNowRestClientMappingProfile(), new InfrastructureMappingProfile());
+builder.Services.AddDefaultServices(builder.Configuration, builder.Logging, new GeneratedServiceNowRestClientMappingProfile(), new InfrastructureMappingProfile());
builder.Services.AddWebApiSampleInfrastructureRepositories();
builder.Services.AddServiceNowRestClientGeneratedRepositories();
builder.Services.AddSqlServerClientClientGeneratedRepositories();
diff --git a/samples/WebApiSample/Properties/launchSettings.json b/samples/WebApiSample/Properties/launchSettings.json
index df81a87..e1ecdaf 100644
--- a/samples/WebApiSample/Properties/launchSettings.json
+++ b/samples/WebApiSample/Properties/launchSettings.json
@@ -1,9 +1,5 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true
- },
"profiles": {
"WebApi": {
"commandName": "Project",
diff --git a/samples/WebApiSample/WebApiSample.csproj b/samples/WebApiSample/WebApiSample.csproj
index b053e80..64af515 100644
--- a/samples/WebApiSample/WebApiSample.csproj
+++ b/samples/WebApiSample/WebApiSample.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/samples/WebApiSample/appsettings.json b/samples/WebApiSample/appsettings.json
index 76c2da8..f87699f 100644
--- a/samples/WebApiSample/appsettings.json
+++ b/samples/WebApiSample/appsettings.json
@@ -1,8 +1,20 @@
{
"AllowedHosts": "*",
"Application": {
- "IsSwaggerEnabled": false,
- "IsHttpsEnforced": true
+ "IsHttpsEnforced": true,
+ "IsOpenTelemetryEnabled": false,
+ "IsSecuredByAzureAd": true,
+ "IsSwaggerEnabled": false
+ },
+ "AzureAd": {
+ "Instance": "https://login.microsoftonline.com/",
+ "CallbackPath": "/signin-oidc",
+ "SignedOutCallbackPath": "/signout-callback-oidc"
+ },
+ "Cache": {
+ "InMemory": {
+ "GeneralTimeoutInSeconds": 3600
+ }
},
"Logging": {
"LogLevel": {
@@ -11,9 +23,17 @@
"RabbidsIncubator": "Information"
}
},
- "Cache": {
- "InMemory": {
- "GeneralTimeoutInSeconds": 3600
+ "OpenTelemetry": {
+ "Service": "SampleServiceNowRestClient",
+ "Metrics": {
+ "Meter": "SampleServiceNowRestClientMetrics"
+ },
+ "Tracing": {
+ "Source": "SampleServiceNowRestClientTracing"
}
+ },
+ "OpenApi": {
+ "Title": "ServiceNow Client Web API sample",
+ "Version": "v1.0"
}
}
diff --git a/samples/WebApiSample/entities.yml b/samples/WebApiSample/entities.yml
index e8c86ba..2824cba 100644
--- a/samples/WebApiSample/entities.yml
+++ b/samples/WebApiSample/entities.yml
@@ -5,6 +5,7 @@ targetApplication: WebApp
entities:
- name: Location
resourceName: locations
+ isAuthorizationRequired: false
queries:
findAll:
serviceNowRestApiTable: cmn_location
@@ -21,6 +22,7 @@ entities:
mapFrom: longitude
- name: DatabaseInventory
resourceName: db-inventories
+ isAuthorizationRequired: false
queries:
findAll:
sqlServerDatabaseTable: Inventory
diff --git a/src/Application.Generators/Application.Generators.csproj b/src/Application.Generators/Application.Generators.csproj
index 9e3d182..61a74e1 100644
--- a/src/Application.Generators/Application.Generators.csproj
+++ b/src/Application.Generators/Application.Generators.csproj
@@ -16,9 +16,9 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
diff --git a/src/Application.Generators/ControllerGenerator.cs b/src/Application.Generators/ControllerGenerator.cs
index d94130e..31cf669 100644
--- a/src/Application.Generators/ControllerGenerator.cs
+++ b/src/Application.Generators/ControllerGenerator.cs
@@ -26,6 +26,7 @@ private static void GenerateController(GeneratorExecutionContext context, Models
var sourceBuilder = new StringBuilder($@"
using System.Collections.Generic;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using RabbidsIncubator.ServiceNowClient.Domain.Models;
@@ -33,18 +34,24 @@ private static void GenerateController(GeneratorExecutionContext context, Models
using {namespaces.Root}.Domain.Repositories;
namespace {namespaces.WebApi}.Controllers
-{{
+{{");
+
+ if (entity.IsAuthorizationRequired)
+ {
+ sourceBuilder.Append(@"
+ [Authorize]");
+ }
+
+ sourceBuilder.Append($@"
[ApiController]
[Route(""{entity.ResourceName}"")]
- public partial class {entityPascalName}Controller : ControllerBase
+ public partial class {entityPascalName}Controller : RabbidsIncubator.ServiceNowClient.Application.Mvc.ControllerBase
{{
- private readonly ILogger _logger;
-
private readonly I{entityPascalName}Repository _{entityCamelName}Repository;
public {entityPascalName}Controller(ILogger<{entityPascalName}Controller> logger, I{entityPascalName}Repository {entityCamelName}Repository)
+ : base(logger)
{{
- _logger = logger;
_{entityCamelName}Repository = {entityCamelName}Repository;
}}
");
@@ -56,7 +63,7 @@ public partial class {entityPascalName}Controller : ControllerBase
public async Task> Get(int? startIndex, int? limit)
{{
var items = await _{entityCamelName}Repository.FindAllAsync(new QueryModel<{entityPascalName}Model>(null, startIndex, limit));
- _logger.LogDebug(""Number of items found: {{itemsCount}}"", items.Count);
+ ReportListCount(items.Count);
return items;
}}
}}
@@ -70,7 +77,7 @@ public partial class {entityPascalName}Controller : ControllerBase
public async Task> Get([FromQuery] {entityPascalName}Model model, int? startIndex, int? limit)
{{
var items = await _{entityCamelName}Repository.FindAllAsync(new QueryModel<{entityPascalName}Model>(model, startIndex, limit));
- _logger.LogDebug(""Number of items found: {{itemsCount}}"", items.Count);
+ ReportListCount(items.Count);
return items;
}}
}}
diff --git a/src/Application.Generators/Models/EntityModel.cs b/src/Application.Generators/Models/EntityModel.cs
index 654d021..ed4d675 100644
--- a/src/Application.Generators/Models/EntityModel.cs
+++ b/src/Application.Generators/Models/EntityModel.cs
@@ -18,6 +18,11 @@ public class EntityModel
///
public string ResourceName { get; set; }
+ ///
+ /// Is an authorization required to access this resource?
+ ///
+ public bool IsAuthorizationRequired { get; set; } = true;
+
///
/// Queries.
///
diff --git a/src/Application.Generators/RepositoryGenerator.cs b/src/Application.Generators/RepositoryGenerator.cs
index 5008876..c4a4247 100644
--- a/src/Application.Generators/RepositoryGenerator.cs
+++ b/src/Application.Generators/RepositoryGenerator.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
@@ -83,6 +83,7 @@ private static void GenerateServiceNowRestClientRepository(GeneratorExecutionCon
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.Logging;
+using RabbidsIncubator.ServiceNowClient.Domain.Diagnostics;
using RabbidsIncubator.ServiceNowClient.Domain.Models;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient;
using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.Repositories;
@@ -98,8 +99,9 @@ public partial class {entityPascalName}Repository : ServiceNowRestClientReposito
ILogger<{entityPascalName}Repository> logger,
IHttpClientFactory httpClientFactory,
IMapper mapper,
- ServiceNowRestClientConfiguration restApiConfiguration)
- : base(logger, httpClientFactory, mapper, restApiConfiguration)
+ ServiceNowRestClientConfiguration restApiConfiguration,
+ IMetricsContext metricsContext)
+ : base(logger, httpClientFactory, mapper, restApiConfiguration, metricsContext)
{{
}}
");
diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
index efbfd5e..462eb39 100644
--- a/src/Application/Application.csproj
+++ b/src/Application/Application.csproj
@@ -14,7 +14,17 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Application/Builder/ApplicationBuilderExtensions.cs b/src/Application/Builder/ApplicationBuilderExtensions.cs
new file mode 100644
index 0000000..09dd735
--- /dev/null
+++ b/src/Application/Builder/ApplicationBuilderExtensions.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Builder;
+using RabbidsIncubator.ServiceNowClient.Application.Middlewares;
+
+namespace RabbidsIncubator.ServiceNowClient.Application.Builder
+{
+ public static class ApplicationBuilderExtensions
+ {
+ public static IApplicationBuilder UseActivityEnrichment(
+ this IApplicationBuilder builder)
+ {
+ return builder.UseMiddleware();
+ }
+ }
+}
diff --git a/src/Application/Builder/WebApplicationExtensions.cs b/src/Application/Builder/WebApplicationExtensions.cs
index 0d27c2b..8685672 100644
--- a/src/Application/Builder/WebApplicationExtensions.cs
+++ b/src/Application/Builder/WebApplicationExtensions.cs
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
+using Microsoft.OpenApi.Models;
+using RabbidsIncubator.ServiceNowClient.Application.Configuration;
namespace RabbidsIncubator.ServiceNowClient.Application.Builder
{
@@ -16,17 +18,25 @@ public static WebApplication AddDefaultMiddlewares(
this WebApplication app,
ConfigurationManager configuration)
{
- if (bool.TryParse(configuration["Application:IsSwaggerEnabled"], out var isSwaggerEnabled) && isSwaggerEnabled)
+ if (bool.TryParse(configuration[ConfigurationConstants.IsSwaggerEnabledConfigKey], out var isSwaggerEnabled) && isSwaggerEnabled)
{
+ var openApi = configuration.GetSectionValue(ConfigurationConstants.OpenApiConfigKey);
app.UseSwagger();
- app.UseSwaggerUI();
+ app.UseSwaggerUI(c => c.SwaggerEndpoint($"/swagger/{openApi.Version}/swagger.json",
+ $"{openApi.Title} {openApi.Version}"));
}
- if (bool.TryParse(configuration["Application:IsHttpsEnforced"], out var isHttpsEnforced) && isHttpsEnforced)
+ if (bool.TryParse(configuration[ConfigurationConstants.IsHttpsEnforcedConfigKey], out var isHttpsEnforced) && isHttpsEnforced)
{
app.UseHttpsRedirection();
}
+ if (bool.TryParse(configuration[ConfigurationConstants.IsSecuredByAzureAdConfigKey], out var isSecuredByAzureAd) && isSecuredByAzureAd)
+ {
+ app.UseAuthentication();
+ }
+
+ app.UseActivityEnrichment();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
diff --git a/src/Application/Configuration/ConfigurationExtensions.cs b/src/Application/Configuration/ConfigurationExtensions.cs
new file mode 100644
index 0000000..c6ff1d0
--- /dev/null
+++ b/src/Application/Configuration/ConfigurationExtensions.cs
@@ -0,0 +1,26 @@
+using System;
+using Microsoft.Extensions.Configuration;
+
+namespace RabbidsIncubator.ServiceNowClient.Application.Configuration
+{
+ public static class ConfigurationExtensions
+ {
+ public static T TryGetSection(this IConfiguration configuration, string sectionKey)
+ {
+ var section = configuration.GetSection(sectionKey);
+ return section.Get();
+ }
+
+ public static T GetSectionValue(this IConfiguration configuration, string sectionKey)
+ {
+ var value = configuration.TryGetSection(sectionKey);
+ if (value == null)
+ {
+ throw new ArgumentException($"Invalid configuration section \"{sectionKey}\" for type \"{typeof(T)}\"",
+ nameof(sectionKey));
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Application/ConfigurationConstants.cs b/src/Application/ConfigurationConstants.cs
new file mode 100644
index 0000000..f0674ef
--- /dev/null
+++ b/src/Application/ConfigurationConstants.cs
@@ -0,0 +1,31 @@
+namespace RabbidsIncubator.ServiceNowClient.Application
+{
+ public static class ConfigurationConstants
+ {
+ public const string IsHttpsEnforcedConfigKey = "Application:IsHttpsEnforced";
+
+ public const string IsSecuredByAzureAdConfigKey = "Application:IsSecuredByAzureAd";
+
+ public const string IsSwaggerEnabledConfigKey = "Application:IsSwaggerEnabled";
+
+ public const string IsOpenTelemetryEnabledConfigKey = "Application:IsOpenTelemetryEnabled";
+
+ public const string AzureAdConfigKey = "AzureAd";
+
+ public const string InMemoryCacheConfigKey = "Cache:InMemory";
+
+ public const string OpenApiConfigKey = "OpenApi";
+
+ public const string OpenTelemetryMetricsMeterConfigKey = "OpenTelemetry:Metrics:Meter";
+
+ public const string OpenTelemetryOtlpExporterEndpointConfigKey = "OpenTelemetry:OtlpExporter:Endpoint";
+
+ public const string OpenTelemetryServiceConfigKey = "OpenTelemetry:Service";
+
+ public const string OpenTelemetryTracingSourceConfigKey = "OpenTelemetry:Tracing:Source";
+
+ public const string ServiceNowRestApiConfigKey = "ServiceNow:RestApi";
+
+ public const string ServiceNowSqlServerConfigKey = "ServiceNow:SqlServer";
+ }
+}
diff --git a/src/Application/DependencyInjection/AutoMapperExtensions.cs b/src/Application/DependencyInjection/AutoMapperExtensions.cs
deleted file mode 100644
index 2a6366c..0000000
--- a/src/Application/DependencyInjection/AutoMapperExtensions.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using AutoMapper;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace RabbidsIncubator.ServiceNowClient.Application.DependencyInjection
-{
- public static class AutoMapperExtensions
- {
- ///
- /// Add AutoMapper configuration in service collection.
- ///
- /// Service collection
- /// Additional profiles
- ///
- public static IServiceCollection AddAutoMapperConfiguration(this IServiceCollection services, params Profile[] additionalProfiles)
- {
- var mappingConfig = new MapperConfiguration(x =>
- {
- x.AddProfile(new Infrastructure.ServiceNowRestClient.MappingProfiles.ServiceNowRestClientMappingProfile());
- x.AddProfile(new Infrastructure.SqlServerClient.MappingProfiles.SqlServerClientMappingProfile());
- if (additionalProfiles != null && additionalProfiles.Length > 0)
- {
- x.AddProfiles(additionalProfiles);
- }
- x.AllowNullCollections = true;
- });
-
- var mapper = mappingConfig.CreateMapper();
-
- mapper.ConfigurationProvider.AssertConfigurationIsValid();
- services.AddSingleton(mapper);
- return services;
- }
- }
-}
diff --git a/src/Application/DependencyInjection/ServiceCollectionExtensions.cs b/src/Application/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..b375828
--- /dev/null
+++ b/src/Application/DependencyInjection/ServiceCollectionExtensions.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using AutoMapper;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Identity.Web;
+using Microsoft.OpenApi.Models;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+using RabbidsIncubator.ServiceNowClient.Application.Configuration;
+using RabbidsIncubator.ServiceNowClient.Infrastructure.InMemory.DependencyInjection;
+using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.DependencyInjection;
+using RabbidsIncubator.ServiceNowClient.Infrastructure.SqlServerClient.DependencyInjection;
+
+namespace RabbidsIncubator.ServiceNowClient.Application.DependencyInjection
+{
+ public static class ServiceCollectionExtensions
+ {
+ ///
+ /// Add default services in the service collection.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IServiceCollection AddDefaultServices(this IServiceCollection services, ConfigurationManager configuration, ILoggingBuilder logging,
+ params Profile[] additionalProfiles)
+ {
+ services.AddAuthentication(configuration, out var isSecuredByAzureAd);
+ services.AddOpenTelemetry(configuration, logging);
+ services.AddAutoMapper(additionalProfiles);
+ services.AddRepositories(configuration);
+ services.AddControllers();
+ services.AddEndpointsApiExplorer();
+ services.AddSwaggerGenWithOpenApiInfo(configuration, isSecuredByAzureAd);
+ services.AddHealthChecks();
+ return services;
+ }
+
+ private static IServiceCollection AddAuthentication(this IServiceCollection services, ConfigurationManager configuration, out bool isSecuredByAzureAd)
+ {
+ if (bool.TryParse(configuration[ConfigurationConstants.IsSecuredByAzureAdConfigKey], out isSecuredByAzureAd) && isSecuredByAzureAd)
+ {
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddMicrosoftIdentityWebApi(configuration.GetSection(ConfigurationConstants.AzureAdConfigKey))
+ .EnableTokenAcquisitionToCallDownstreamApi()
+ .AddInMemoryTokenCaches();
+ }
+
+ return services;
+ }
+
+ private static IServiceCollection AddOpenTelemetry(this IServiceCollection services, ConfigurationManager configuration, ILoggingBuilder logging,
+ Action? enrichAction = default)
+ {
+ if (bool.TryParse(configuration[ConfigurationConstants.IsOpenTelemetryEnabledConfigKey], out var isOpenTelemetryEnabled) && isOpenTelemetryEnabled)
+ {
+ var openTelemetryCollectorEndpoint = configuration[ConfigurationConstants.OpenTelemetryOtlpExporterEndpointConfigKey];
+ var openTelemetryService = configuration[ConfigurationConstants.OpenTelemetryServiceConfigKey];
+
+ var openTelemetryMetricsMeter = configuration[ConfigurationConstants.OpenTelemetryMetricsMeterConfigKey];
+ if (!string.IsNullOrEmpty(openTelemetryMetricsMeter))
+ {
+ services.AddSingleton();
+ services.AddOpenTelemetryMetrics(builder =>
+ {
+ builder.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(openTelemetryService));
+ builder.AddAspNetCoreInstrumentation();
+ builder.AddHttpClientInstrumentation();
+ builder.AddMeter(openTelemetryMetricsMeter);
+ builder.AddOtlpExporter(options => options.Endpoint = new Uri(openTelemetryCollectorEndpoint));
+ });
+ }
+
+ var openTelemetryTracingSource = configuration[ConfigurationConstants.OpenTelemetryTracingSourceConfigKey];
+ if (!string.IsNullOrEmpty(openTelemetryTracingSource))
+ {
+ services.AddOpenTelemetryTracing(builder =>
+ {
+ builder.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(openTelemetryService));
+ builder.AddAspNetCoreInstrumentation(options =>
+ {
+ options.RecordException = true;
+ if (enrichAction != default)
+ {
+ options.Enrich = enrichAction;
+ }
+ });
+ builder.AddHttpClientInstrumentation();
+ builder.AddSqlClientInstrumentation();
+ builder.AddSource(openTelemetryTracingSource);
+ builder.AddOtlpExporter(options => options.Endpoint = new Uri(openTelemetryCollectorEndpoint));
+ });
+ }
+
+ logging.AddOpenTelemetry(builder =>
+ {
+ builder.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(openTelemetryService));
+ builder.IncludeFormattedMessage = true;
+ builder.IncludeScopes = true;
+ builder.ParseStateValues = true;
+ builder.AddOtlpExporter(options => options.Endpoint = new Uri(openTelemetryCollectorEndpoint));
+ });
+ }
+
+ return services;
+ }
+
+ ///
+ /// Add AutoMapper configuration in service collection.
+ ///
+ /// Service collection
+ /// Additional profiles
+ ///
+ public static IServiceCollection AddAutoMapper(this IServiceCollection services, params Profile[] additionalProfiles)
+ {
+ var mappingConfig = new MapperConfiguration(x =>
+ {
+ x.AddProfile(new Infrastructure.ServiceNowRestClient.MappingProfiles.ServiceNowRestClientMappingProfile());
+ x.AddProfile(new Infrastructure.SqlServerClient.MappingProfiles.SqlServerClientMappingProfile());
+ if (additionalProfiles != null && additionalProfiles.Length > 0)
+ {
+ x.AddProfiles(additionalProfiles);
+ }
+ x.AllowNullCollections = true;
+ });
+
+ var mapper = mappingConfig.CreateMapper();
+
+ mapper.ConfigurationProvider.AssertConfigurationIsValid();
+ services.AddSingleton(mapper);
+ return services;
+ }
+
+ private static IServiceCollection AddRepositories(this IServiceCollection services, ConfigurationManager configuration)
+ {
+ services.AddInMemoryRepositories(configuration.GetSectionValue(ConfigurationConstants.InMemoryCacheConfigKey));
+
+ services.AddServiceNowRestClientRepositories(configuration.GetSectionValue(ConfigurationConstants.ServiceNowRestApiConfigKey));
+ if (configuration.TryGetSection(ConfigurationConstants.ServiceNowSqlServerConfigKey) != null)
+ {
+ services.AddSqlServerClientRepositories(configuration.GetSectionValue(ConfigurationConstants.ServiceNowSqlServerConfigKey));
+ }
+
+ return services;
+ }
+
+ private static IServiceCollection AddSwaggerGenWithOpenApiInfo(this IServiceCollection services, ConfigurationManager configuration, bool isSecured)
+ {
+ services.AddSwaggerGen(c =>
+ {
+ var openApi = configuration.GetSectionValue(ConfigurationConstants.OpenApiConfigKey);
+ c.SwaggerDoc(openApi.Version, openApi);
+ if (isSecured)
+ {
+ c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
+ {
+ Description = "JWT Authorization header using the Bearer scheme.\r\n\r\nEnter your token in the text input below.",
+ Name = "Authorization",
+ In = ParameterLocation.Header,
+ Type = SecuritySchemeType.Http,
+ Scheme = "Bearer"
+ });
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "Bearer"
+ },
+ Name = "Bearer",
+ In = ParameterLocation.Header,
+ },
+ new List()
+ }
+ });
+ }
+ });
+
+ return services;
+ }
+ }
+}
diff --git a/src/Application/DependencyInjection/WebApplicationServicesExtensions.cs b/src/Application/DependencyInjection/WebApplicationServicesExtensions.cs
deleted file mode 100644
index 7866445..0000000
--- a/src/Application/DependencyInjection/WebApplicationServicesExtensions.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using RabbidsIncubator.ServiceNowClient.Infrastructure.InMemory.DependencyInjection;
-using RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.DependencyInjection;
-using RabbidsIncubator.ServiceNowClient.Infrastructure.SqlServerClient.DependencyInjection;
-
-namespace RabbidsIncubator.ServiceNowClient.Application.DependencyInjection
-{
- public static class WebApplicationServicesExtensions
- {
- public const string InMemoryCacheConfigKey = "Cache:InMemory";
-
- public const string RestApiServiceNowConfigKey = "ServiceNow:RestApi";
-
- public const string SqlServerServiceNowConfigKey = "ServiceNow:SqlServer";
-
- ///
- /// Add default services in the service collection.
- /// Expected configuration elements: "Cache:InMemory", "ServiceNow:RestApi", "ServiceNow:SqlServer".
- ///
- ///
- ///
- ///
- ///
- public static IServiceCollection AddDefaultServices(
- this IServiceCollection services,
- ConfigurationManager configuration,
- params AutoMapper.Profile[] additionalProfiles)
- {
- services.AddAutoMapperConfiguration(additionalProfiles);
- services.AddInMemoryRepositories(configuration.GetSectionValue(InMemoryCacheConfigKey));
- services.AddServiceNowRestClientRepositories(configuration.GetSectionValue(RestApiServiceNowConfigKey));
- if (configuration.TryGetSection(SqlServerServiceNowConfigKey) != null)
- {
- services.AddSqlServerClientRepositories(configuration.GetSectionValue(SqlServerServiceNowConfigKey));
- }
- services.AddControllers();
- services.AddEndpointsApiExplorer();
- services.AddSwaggerGen();
- services.AddHealthChecks();
- return services;
- }
-
- public static T TryGetSection(this IConfiguration configuration, string sectionKey)
- {
- var section = configuration.GetSection(sectionKey);
- return section.Get();
- }
-
- public static T GetSectionValue(this IConfiguration configuration, string sectionKey)
- {
- var value = configuration.TryGetSection(sectionKey);
- if (value == null)
- {
- throw new ArgumentException($"Missing section \"{sectionKey}\" in configuration");
- }
-
- return value;
- }
- }
-}
diff --git a/src/Application/Diagnostics/MetricsContext.cs b/src/Application/Diagnostics/MetricsContext.cs
new file mode 100644
index 0000000..c0ad9b9
--- /dev/null
+++ b/src/Application/Diagnostics/MetricsContext.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Diagnostics.Metrics;
+using Microsoft.Extensions.Configuration;
+using RabbidsIncubator.ServiceNowClient.Domain.Diagnostics;
+
+namespace RabbidsIncubator.ServiceNowClient.Application.Diagnostics
+{
+ public class MetricsContext : IMetricsContext
+ {
+ private readonly string _name;
+
+ private readonly Dictionary _instruments = new();
+
+ private Meter? _meter;
+
+ public MetricsContext(IConfiguration configuration)
+ {
+ _name = configuration[ConfigurationConstants.OpenTelemetryMetricsMeterConfigKey];
+ }
+
+ public Meter Meter
+ {
+ get
+ {
+ if (_meter == null)
+ {
+ _meter = new Meter(_name);
+ }
+ return _meter;
+ }
+ }
+
+ public void AddToCounter(string name, T value, KeyValuePair? tag = null)
+ where T : struct
+ {
+ if (!_instruments.ContainsKey(name))
+ {
+ CreateCounter(name);
+ }
+
+ if (_instruments.ContainsKey(name) && _instruments[name] is Counter counter)
+ {
+ if (tag == null)
+ {
+ counter.Add(value);
+ }
+ else
+ {
+ counter.Add(value, tag.Value);
+ }
+ }
+ }
+
+ private Counter CreateCounter(string name)
+ where T : struct
+ {
+ var counter = Meter.CreateCounter(name);
+ _instruments.Add(name, counter);
+ return counter;
+ }
+ }
+}
diff --git a/src/Application/Middlewares/DiagnosticsEnrichmentMiddleware.cs b/src/Application/Middlewares/DiagnosticsEnrichmentMiddleware.cs
new file mode 100644
index 0000000..e063e6e
--- /dev/null
+++ b/src/Application/Middlewares/DiagnosticsEnrichmentMiddleware.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace RabbidsIncubator.ServiceNowClient.Application.Middlewares
+{
+ ///
+ /// Middleware that will add information to diagnostics activity.
+ ///
+ ///
+ /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write
+ ///
+ public class DiagnosticsEnrichmentMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ public DiagnosticsEnrichmentMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async Task InvokeAsync(HttpContext context, Domain.Diagnostics.IMetricsContext metricsContext)
+ {
+ metricsContext.AddToCounter("http.requests", 1, KeyValuePair.Create("path", context.Request.Path.ToString()));
+
+ Activity.Current?.AddTag("user.name", (context.User.Identity as ClaimsIdentity)?.FindFirst("name")?.Value);
+
+ await _next(context);
+ }
+ }
+}
diff --git a/src/Application/Mvc/ControllerBase.cs b/src/Application/Mvc/ControllerBase.cs
new file mode 100644
index 0000000..370541e
--- /dev/null
+++ b/src/Application/Mvc/ControllerBase.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+
+namespace RabbidsIncubator.ServiceNowClient.Application.Mvc
+{
+ public abstract class ControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase
+ {
+ protected ILogger Logger { get; private set; }
+
+ protected ControllerBase(ILogger logger)
+ {
+ Logger = logger;
+ }
+
+ protected void ReportListCount(int count)
+ {
+ Logger.LogDebug("Number of items found: {itemsCount}", count);
+ Activity.Current?.AddTag("response.list_size", count);
+ }
+ }
+}
diff --git a/src/ConsoleApp/ConsoleApp.csproj b/src/ConsoleApp/ConsoleApp.csproj
index 012d701..85f7d58 100644
--- a/src/ConsoleApp/ConsoleApp.csproj
+++ b/src/ConsoleApp/ConsoleApp.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/src/ConsoleApp/Program.cs b/src/ConsoleApp/Program.cs
index 167c5cc..722bd67 100644
--- a/src/ConsoleApp/Program.cs
+++ b/src/ConsoleApp/Program.cs
@@ -74,7 +74,7 @@ private static ServiceProvider CreateServiceProvider(CommandLineOptions opts, Ap
})
.AddServiceNowRestClientGeneratedRepositories()
.AddServiceNowRestClientRepositories(appConfiguration.ServiceNowRestClientConfiguration)
- .AddAutoMapperConfiguration(new GeneratedServiceNowRestClientMappingProfile());
+ .AddAutoMapper(new GeneratedServiceNowRestClientMappingProfile());
return serviceCollection.BuildServiceProvider();
}
diff --git a/src/Domain/Diagnostics/IMetricsContext.cs b/src/Domain/Diagnostics/IMetricsContext.cs
new file mode 100644
index 0000000..319acc2
--- /dev/null
+++ b/src/Domain/Diagnostics/IMetricsContext.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace RabbidsIncubator.ServiceNowClient.Domain.Diagnostics
+{
+ public interface IMetricsContext
+ {
+ void AddToCounter(string name, T value, KeyValuePair? tag = null) where T : struct;
+ }
+}
diff --git a/src/Infrastructure.InMemory/Infrastructure.InMemory.csproj b/src/Infrastructure.InMemory/Infrastructure.InMemory.csproj
index b90d338..be81851 100644
--- a/src/Infrastructure.InMemory/Infrastructure.InMemory.csproj
+++ b/src/Infrastructure.InMemory/Infrastructure.InMemory.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/Infrastructure.ServiceNowRestClient/Infrastructure.ServiceNowRestClient.csproj b/src/Infrastructure.ServiceNowRestClient/Infrastructure.ServiceNowRestClient.csproj
index 21d36f1..53ce6a5 100644
--- a/src/Infrastructure.ServiceNowRestClient/Infrastructure.ServiceNowRestClient.csproj
+++ b/src/Infrastructure.ServiceNowRestClient/Infrastructure.ServiceNowRestClient.csproj
@@ -14,11 +14,11 @@
-
+
-
-
-
+
+
+
diff --git a/src/Infrastructure.ServiceNowRestClient/Repositories/ServiceNowRestClientRepositoryBase.cs b/src/Infrastructure.ServiceNowRestClient/Repositories/ServiceNowRestClientRepositoryBase.cs
index 29572a7..0fe86da 100644
--- a/src/Infrastructure.ServiceNowRestClient/Repositories/ServiceNowRestClientRepositoryBase.cs
+++ b/src/Infrastructure.ServiceNowRestClient/Repositories/ServiceNowRestClientRepositoryBase.cs
@@ -4,6 +4,7 @@
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.Logging;
+using RabbidsIncubator.ServiceNowClient.Domain.Diagnostics;
namespace RabbidsIncubator.ServiceNowClient.Infrastructure.ServiceNowRestClient.Repositories
{
@@ -21,17 +22,21 @@ protected ServiceNowRestClientRepositoryBase(
ILogger logger,
IHttpClientFactory httpClientFactory,
IMapper mapper,
- ServiceNowRestClientConfiguration restApiConfiguration)
+ ServiceNowRestClientConfiguration restApiConfiguration,
+ IMetricsContext metricsContext)
: base(logger, httpClientFactory)
{
Mapper = mapper;
_restApiConfiguration = restApiConfiguration;
+ MetricsContext = metricsContext;
}
protected override string HttpClientName => _restApiConfiguration.HttpClientName;
protected IMapper Mapper { get; private set; }
+ protected IMetricsContext MetricsContext { get; private set; }
+
///
/// Generate URL from parameters.
///
@@ -90,6 +95,7 @@ protected async Task> FindAllAsync(string tableName, Domain.Models
where T : class
where U : Dto.IEntityDto
{
+ MetricsContext.AddToCounter("servicenow.restapi_requests", 1);
var url = GenerateUrl(tableName, queryModel, extraParameters);
var resultList = await GetAsync>(url);
return Mapper.Map>(resultList.Result);
diff --git a/src/Infrastructure.SqlServerClient/Infrastructure.SqlServerClient.csproj b/src/Infrastructure.SqlServerClient/Infrastructure.SqlServerClient.csproj
index 7bc9caa..6d257e8 100644
--- a/src/Infrastructure.SqlServerClient/Infrastructure.SqlServerClient.csproj
+++ b/src/Infrastructure.SqlServerClient/Infrastructure.SqlServerClient.csproj
@@ -14,9 +14,9 @@
-
+
-
+
diff --git a/src/Infrastructure.SqlServerClient/Repositories/SqlServerClientRepositoryBase.cs b/src/Infrastructure.SqlServerClient/Repositories/SqlServerClientRepositoryBase.cs
index a5245f8..7e81994 100644
--- a/src/Infrastructure.SqlServerClient/Repositories/SqlServerClientRepositoryBase.cs
+++ b/src/Infrastructure.SqlServerClient/Repositories/SqlServerClientRepositoryBase.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
diff --git a/test/Application.Generators.UnitTests/Application.Generators.UnitTests.csproj b/test/Application.Generators.UnitTests/Application.Generators.UnitTests.csproj
index f2c8b48..4d1aa6b 100644
--- a/test/Application.Generators.UnitTests/Application.Generators.UnitTests.csproj
+++ b/test/Application.Generators.UnitTests/Application.Generators.UnitTests.csproj
@@ -15,16 +15,16 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/Application.Generators.UnitTests/ControllerGeneratorTest.cs b/test/Application.Generators.UnitTests/ControllerGeneratorTest.cs
index 3a502c0..dd1a66e 100644
--- a/test/Application.Generators.UnitTests/ControllerGeneratorTest.cs
+++ b/test/Application.Generators.UnitTests/ControllerGeneratorTest.cs
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
+using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
@@ -16,6 +17,8 @@ public class ControllerGeneratorTest
public async Task ControllerGeneratorGenerateCode()
{
var original = @"
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
using RabbidsIncubator.ServiceNowClient.Domain.Models;
@@ -32,10 +35,28 @@ public interface ILocationRepository
Task> FindAllAsync(QueryModel query);
}
}
+namespace RabbidsIncubator.ServiceNowClient.Application.Mvc
+{
+ public abstract class ControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase
+ {
+ protected ILogger Logger { get; private set; }
+
+ protected ControllerBase(ILogger logger)
+ {
+ Logger = logger;
+ }
+
+ protected void ReportListCount(int count)
+ {
+ Logger.LogDebug(""Dummy log"");
+ }
+ }
+}
";
var expected = @"
using System.Collections.Generic;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using RabbidsIncubator.ServiceNowClient.Domain.Models;
@@ -44,17 +65,16 @@ public interface ILocationRepository
namespace RabbidsIncubator.ServiceNowClient.DummyProject.Controllers
{
+ [Authorize]
[ApiController]
[Route(""locations"")]
- public partial class LocationController : ControllerBase
+ public partial class LocationController : RabbidsIncubator.ServiceNowClient.Application.Mvc.ControllerBase
{
- private readonly ILogger _logger;
-
private readonly ILocationRepository _locationRepository;
public LocationController(ILogger logger, ILocationRepository locationRepository)
+ : base(logger)
{
- _logger = logger;
_locationRepository = locationRepository;
}
@@ -62,7 +82,7 @@ public LocationController(ILogger logger, ILocationRepositor
public async Task> Get([FromQuery] LocationModel model, int? startIndex, int? limit)
{
var items = await _locationRepository.FindAllAsync(new QueryModel(model, startIndex, limit));
- _logger.LogDebug(""Number of items found: {itemsCount}"", items.Count);
+ ReportListCount(items.Count);
return items;
}
}
diff --git a/test/Application.Generators.UnitTests/ModelGeneratorTest.cs b/test/Application.Generators.UnitTests/ModelGeneratorTest.cs
index 9a3b176..cfa4ef9 100644
--- a/test/Application.Generators.UnitTests/ModelGeneratorTest.cs
+++ b/test/Application.Generators.UnitTests/ModelGeneratorTest.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
diff --git a/test/Application.UnitTests/Application.UnitTests.csproj b/test/Application.UnitTests/Application.UnitTests.csproj
index 34dba92..20a0ce4 100644
--- a/test/Application.UnitTests/Application.UnitTests.csproj
+++ b/test/Application.UnitTests/Application.UnitTests.csproj
@@ -15,14 +15,14 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/Application.UnitTests/DependencyInjection/AutoMapperExtensionsTest.cs b/test/Application.UnitTests/DependencyInjection/AutoMapperExtensionsTest.cs
index c5550bf..adace68 100644
--- a/test/Application.UnitTests/DependencyInjection/AutoMapperExtensionsTest.cs
+++ b/test/Application.UnitTests/DependencyInjection/AutoMapperExtensionsTest.cs
@@ -9,13 +9,13 @@ namespace RabbidsIncubator.ServiceNowClient.Application.UnitTests.DependencyInje
public class AutoMapperExtensionsTest
{
[Fact]
- public void AutoMapperExtensions_AddAutoMapperConfiguration_RegisterIMapper()
+ public void AutoMapperExtensions_AddAutoMapper_RegisterIMapper()
{
// Arrange
var serviceCollection = new ServiceCollection();
// Act
- serviceCollection.AddAutoMapperConfiguration();
+ serviceCollection.AddAutoMapper();
// Assert
var serviceProvider = serviceCollection.BuildServiceProvider();
diff --git a/test/Infrastructure.ServiceNowRestClient.UnitTests/Infrastructure.ServiceNowRestClient.UnitTests.csproj b/test/Infrastructure.ServiceNowRestClient.UnitTests/Infrastructure.ServiceNowRestClient.UnitTests.csproj
index db440f7..3e3d63c 100644
--- a/test/Infrastructure.ServiceNowRestClient.UnitTests/Infrastructure.ServiceNowRestClient.UnitTests.csproj
+++ b/test/Infrastructure.ServiceNowRestClient.UnitTests/Infrastructure.ServiceNowRestClient.UnitTests.csproj
@@ -15,13 +15,13 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/WebApiSample.IntegrationTests/WebApiSample.IntegrationTests.csproj b/test/WebApiSample.IntegrationTests/WebApiSample.IntegrationTests.csproj
index 1f89ecd..bff383d 100644
--- a/test/WebApiSample.IntegrationTests/WebApiSample.IntegrationTests.csproj
+++ b/test/WebApiSample.IntegrationTests/WebApiSample.IntegrationTests.csproj
@@ -15,15 +15,15 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive