diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 78dfac3a7..1b69a04c8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 +FROM mcr.microsoft.com/dotnet/sdk:8.0 RUN apt-get update diff --git a/.github/workflows/1-pr.yaml b/.github/workflows/1-pr.yaml index c6d8f9f44..b38b914d0 100644 --- a/.github/workflows/1-pr.yaml +++ b/.github/workflows/1-pr.yaml @@ -8,7 +8,7 @@ env: # web app DOCKERFILE_PATH: "CarbonAware.WebApi/src/Dockerfile" HEALTH_ENDPOINT: "0.0.0.0:8080/health" - DLL_FILE_PATH: "./bin/Release/net6.0/CarbonAware.WebApi.dll" + DLL_FILE_PATH: "./bin/Release/net8.0/CarbonAware.WebApi.dll" DOTNET_SRC_DIR: "./src" # console app packages DOTNET_SOLUTION: "src/GSF.CarbonAware/src/GSF.CarbonAware.csproj" @@ -40,9 +40,9 @@ jobs: - uses: actions/checkout@v2 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL @@ -86,14 +86,14 @@ jobs: needs: sln-build-and-test runs-on: ubuntu-latest container: - image: mcr.microsoft.com/dotnet/sdk:6.0 + image: mcr.microsoft.com/dotnet/sdk:8.0 steps: - uses: actions/checkout@v3 - - name: Setup .NET Core SDK 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET Core SDK 8 + uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' include-prerelease: false - name: Install dependencies @@ -123,6 +123,8 @@ jobs: - name: Generate Open API run: dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml ${{ env.DLL_FILE_PATH }} v1 + env: + DOTNET_ROLL_FORWARD: LatestMajor working-directory: ./src/CarbonAware.WebApi/src - name: Upload swagger artifact @@ -144,7 +146,7 @@ jobs: - name: Docker Run Container run: | - docker run -d --name runnable-container -p 8080:80 ca-api + docker run -d --name runnable-container -p 8080:8080 ca-api docker container ls - name: Docker WGET Health Endpoint @@ -164,10 +166,10 @@ jobs: uses: actions/checkout@v3 with: ref: dev - - name: Setup .NET Core SDK 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET Core SDK 8 + uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' include-prerelease: false - name: Install dependencies run: dotnet restore @@ -179,6 +181,8 @@ jobs: working-directory: ${{ env.DOTNET_SRC_DIR }} - name: Generate Open API run: dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml ${{ env.DLL_FILE_PATH }} v1 + env: + DOTNET_ROLL_FORWARD: LatestMajor - name: Upload dev artifact uses: actions/upload-artifact@v1 with: @@ -199,10 +203,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Setup .NET Core SDK 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET Core SDK 8 + uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' include-prerelease: false - name: Create packages @@ -244,4 +248,4 @@ jobs: command: config globs: | ./custom.markdownlint.jsonc - {"*[^.github]/**,*"}.md \ No newline at end of file + {"*[^.github]/**,*"}.md diff --git a/.github/workflows/dev_carbon-aware-api.yml b/.github/workflows/dev_carbon-aware-api.yml index 643d58b8d..f1c4dc4ae 100644 --- a/.github/workflows/dev_carbon-aware-api.yml +++ b/.github/workflows/dev_carbon-aware-api.yml @@ -20,7 +20,7 @@ jobs: - name: Set up .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' include-prerelease: true - name: Build with dotnet diff --git a/.vscode/launch.json b/.vscode/launch.json index 7df71697f..8921c133f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "buildCLI", - "program": "${workspaceFolder}/src/CarbonAware.CLI/src/bin/Debug/net6.0/caw.dll", + "program": "${workspaceFolder}/src/CarbonAware.CLI/src/bin/Debug/net8.0/caw.dll", "args": [ "emissions", "--location", "${input:caw_location}" @@ -27,7 +27,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "buildWebApi", - "program": "${workspaceFolder}/src/CarbonAware.WebApi/src/bin/Debug/net6.0/CarbonAware.WebApi.dll", + "program": "${workspaceFolder}/src/CarbonAware.WebApi/src/bin/Debug/net8.0/CarbonAware.WebApi.dll", "args": [], "cwd": "${workspaceFolder}/src/CarbonAware.WebApi/src/", "stopAtEntry": false, diff --git a/casdk-docs/docs/quickstart.md b/casdk-docs/docs/quickstart.md index ab86b4d97..90e271240 100644 --- a/casdk-docs/docs/quickstart.md +++ b/casdk-docs/docs/quickstart.md @@ -18,7 +18,7 @@ generated libraries for your language of choice! Prerequisites: -- .NET Core 6.0 +- .NET Core 8.0 - Alternatively: - Docker - VSCode (it is recommended to work in a Dev Container) diff --git a/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md b/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md index f971f0714..028db6273 100644 --- a/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md +++ b/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md @@ -456,10 +456,10 @@ CarbonAware.LocationSources.LocationSource: Warning: New key swedencentral_1 gen ## Error Handling The WebAPI leveraged the -[.Net controller filter pipeline](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0) +[.Net controller filter pipeline](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-8.0) to ensure that all requests respond with a consistent JSON schema. -![.Net controller filter pipeline image](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters/_static/filter-pipeline-2.png?view=aspnetcore-6.0) +![.Net controller filter pipeline image](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters/_static/filter-pipeline-2.png?view=aspnetcore-8.0) Controllers are responsible for managing the "Success" responses. If an error occurs in the WebAPI code and an unhandled exception is thrown, the @@ -470,7 +470,7 @@ caught and handled by the WebAPI code, the controller will continue to manage the response. The .Net framework will automatically respond to validation errors with a -[ValidationProblemDetails](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails?view=aspnetcore-6.0) +[ValidationProblemDetails](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails?view=aspnetcore-8.0) object. Using the Exception Filter class enables the WebAPI to consistently respond with the `ValidationProblemDetails` error schema in all error cases and take advantage of error handling automatically provided by the framework. @@ -489,7 +489,7 @@ specification cd CarbonAware.WebApi/src dotnet tool restore dotnet build --configuration Release --no-restore - dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml bin/Release/net6.0/CarbonAware.WebApi.dll v1 + dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml bin/Release/net8.0/CarbonAware.WebApi.dll v1 ``` 1. The `CarbonAware.WebApi/src/wwwroot/api/v1/swagger.yaml` file contains the supported OpenApi specification. diff --git a/casdk-docs/docs/tutorial-basics/containerization.md b/casdk-docs/docs/tutorial-basics/containerization.md index 355a9a9b0..8f8211e05 100644 --- a/casdk-docs/docs/tutorial-basics/containerization.md +++ b/casdk-docs/docs/tutorial-basics/containerization.md @@ -25,11 +25,11 @@ carbon_aware v1 6293e2528bf2 About an hour ago 230MB ## Run WebApi Image 1. Run the image using `docker run` with host port 8000 mapped to the WebApi - port 80 and configure environment variable settings for + port 8080 and configure environment variable settings for [WattTime](https://www.watttime.org) provider. ```sh - docker run --rm -p 8000:80 \ + docker run --rm -p 8000:8080 \ > -e DataSources__EmissionsDataSource="WattTime" \ > -e DataSources__ForecastDataSource="WattTime" \ > -e DataSources__Configurations__WattTime__Type="WattTime" \ @@ -40,7 +40,7 @@ carbon_aware v1 6293e2528bf2 About an hour ago 230MB or the [ElectricityMaps](https://www.electricitymaps.com) provider ```sh - docker run --rm -p 8000:80 \ + docker run --rm -p 8000:8080 \ > -e DataSources__EmissionsDataSource="ElectricityMaps" \ > -e DataSources__ForecastDataSource="ElectricityMaps" \ > -e DataSources__Configurations__ElectricityMaps__Type="ElectricityMaps" \ @@ -52,7 +52,7 @@ carbon_aware v1 6293e2528bf2 About an hour ago 230MB or the [ElectricityMapsFree](https://www.co2signal.com/) provider ```sh - docker run --rm -p 8000:80 \ + docker run --rm -p 8000:8080 \ > -e DataSources__EmissionsDataSource="ElectricityMapsFree" \ > -e DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" \ > -e DataSources__Configurations__ElectricityMapsFree__token="" \ diff --git a/casdk-docs/docs/tutorial-extras/configuration.md b/casdk-docs/docs/tutorial-extras/configuration.md index 7453b8e82..c65aa44f2 100644 --- a/casdk-docs/docs/tutorial-extras/configuration.md +++ b/casdk-docs/docs/tutorial-extras/configuration.md @@ -19,9 +19,11 @@ - [ElectricityMapsFree Configuration](#electricitymapsfree-configuration) - [API Token](#api-token) - [BaseUrl](#baseurl) + - [Cache](#cache) - [CarbonAwareVars](#carbonawarevars) - [Tracing and Monitoring Configuration](#tracing-and-monitoring-configuration) - [Verbosity](#verbosity) + - [Prometheus exporter](#prometheus-exporter-for-emissions-data) - [Web API Prefix](#web-api-prefix) - [LocationDataSourcesConfiguration](#locationdatasourcesconfiguration) - [Sample Configurations](#sample-configurations) @@ -194,7 +196,7 @@ custom `EmissionsData` sets. The file should be located under the `/src/data/data-sources/` directory that is part of the repository. At build time, all the JSON files under `/src/data/data-sources/` are copied over the destination directory -`/src/CarbonAware.WebApi/src/bin/[Debug|Publish]/net6.0/data-sources/json` +`/src/CarbonAware.WebApi/src/bin/[Debug|Publish]/net8.0/data-sources/json` that is part of the `CarbonAware.WebApi` assembly. Also the file can be placed where the assembly `CarbonAware.WebApi.dll` is located under `data-sources/json` directory. For instance, if the application is installed under `/app`, copy the @@ -327,6 +329,29 @@ The url to use when connecting to ElectricityMapsFree. Defaults to "https://api.co2signal.com/v1/" but can be overridden in the config if needed (such as to enable integration testing scenarios). +## Cache + +Frequent access to data sources could cause problems such as performance trouble +or exceed rate limit. To avoid them, you can configure data cache like this: + +```json +{ + "EmissionsDataCache": { + "Enabled": true, + "ExpirationMin": 30 + } +} +``` + +The behavior of current cache implementation: +* Only emissions data are cached + * Forecast data are not stored +* The result of the latest query to data sources is cached +* Use cache rather than data sources if even one datum in cache match with the query + * Even though more data in data sources would be matched, they are not retrieved +* Cached data are stored in memory + * They are cleard when the process of the SDK is down + ## CarbonAwareVars This section contains the global settings for the SDK. The configuration looks @@ -379,6 +404,32 @@ InstrumentationKey. For more details, please refer to AppInsights_InstrumentationKey="AppInsightsInstrumentationKey" ``` +### Prometheus exporter for emissions data + +> DISCLAIMER: The `/metrics` Prometheus exporter is currently unsupported, and is used for internal GSF needs, and may change in the future. It will retrieve _all_ emissions data and create heavy load on your data API's. It is turned off by default. + +In the WebApi project, this application can exporse latest carbon emissions data as a prometheus exporter. + +```bash +CarbonAwareVars__EnableCarbonExporter="true" +``` +The scraping endpoint is `/metrics` like this: + +```bash +http://localhost/metrics +``` + +By default, the exposed data are latest ones within last 24 hours. If you would like to change the period +in some reasones, you can configure the value like this: + +```json +{ + "CarbonExporter": { + "PeriodInHours": 48 + } +} +``` + ### Verbosity You can configure the verbosity of the application error messages by setting the @@ -427,7 +478,7 @@ By setting `LocationDataSourcesConfiguration` property with one or more location data sources, it is possible to load different `Location` data sets in order to have more than one location. For instance by setting two location regions, the property would be set as follow using -[environment](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0#naming-of-environment-variables) +[environment](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0#naming-of-environment-variables) variables: ```sh @@ -458,7 +509,7 @@ curl "http://${IP_HOST}:${PORT}/emissions/bylocations/best?location=${REGION}&ti At build time, all the JSON files under `/src/data/location-sources` are copied over the destination directory -`/src/CarbonAware.WebApi/src/bin/[Debug|Publish]/net6.0/location-sources/json` +`/src/CarbonAware.WebApi/src/bin/[Debug|Publish]/net8.0/location-sources/json` that is part of the `CarbonAware.WebApi` assembly. Also the file can be placed where the assembly `CarbonAware.WebApi.dll` is located under `location-sources/json` directory. For instance, if the application is installed diff --git a/casdk-docs/docs/tutorial-extras/packaging.md b/casdk-docs/docs/tutorial-extras/packaging.md index ac8dac6d7..7aef80801 100644 --- a/casdk-docs/docs/tutorial-extras/packaging.md +++ b/casdk-docs/docs/tutorial-extras/packaging.md @@ -63,7 +63,7 @@ project. When running in the dev container you will need: - [Remote Containers extension for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) Alternatively you can run in your local environment using the -[.NET Core 6.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0). +[.NET Core 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0). ## SDK Configuration diff --git a/global.json b/global.json index 1c64019b5..ad8ad01d1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.418", + "version": "8.0.201", "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml index cec413296..5a21b6682 100644 --- a/helm-chart/templates/deployment.yaml +++ b/helm-chart/templates/deployment.yaml @@ -39,7 +39,7 @@ spec: {{- end }} ports: - name: http - containerPort: 80 + containerPort: 8080 protocol: TCP volumeMounts: - name: appsettings diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 452ef6a21..1f5cdaf9e 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -37,7 +37,7 @@ securityContext: {} service: type: ClusterIP - port: 80 + port: 8080 ingress: enabled: false diff --git a/samples/azure/azure-function/Dockerfile b/samples/azure/azure-function/Dockerfile index 53316cbf0..cc5a6d5f1 100644 --- a/samples/azure/azure-function/Dockerfile +++ b/samples/azure/azure-function/Dockerfile @@ -1,10 +1,10 @@ # Find the Dockerfile at this URL # https://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/dotnet/dotnet-inproc/dotnet.Dockerfile -FROM mcr.microsoft.com/azure-functions/dotnet:4.0 AS base +FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base WORKDIR /home/site/wwwroot -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build COPY ["src/", "data/src/"] COPY ["scripts/", "data/scripts/"] COPY ["samples/", "data/samples/"] @@ -21,3 +21,4 @@ RUN dotnet publish "samples/azure/azure-function/function.csproj" -c Release -o FROM base AS final WORKDIR /home/site/wwwroot COPY --from=publish /app/publish . +ENV ASPNETCORE_CONTENTROOT=/home/site/wwwroot diff --git a/samples/azure/azure-function/GetCarbonIntensity.cs b/samples/azure/azure-function/GetCarbonIntensity.cs index c1e2a441e..7ca8db605 100644 --- a/samples/azure/azure-function/GetCarbonIntensity.cs +++ b/samples/azure/azure-function/GetCarbonIntensity.cs @@ -1,7 +1,7 @@ using GSF.CarbonAware.Handlers; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System; @@ -14,17 +14,18 @@ namespace function public class GetCarbonIntensity { private readonly IEmissionsHandler _handler; + private readonly ILogger _log; - public GetCarbonIntensity(IEmissionsHandler handler) + public GetCarbonIntensity(IEmissionsHandler handler, ILogger log) { this._handler = handler; + this._log = log; } - [FunctionName("GetAverageCarbonIntensity")] + [Function("GetAverageCarbonIntensity")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { //Get the startDate, endDate, and location from the request query if the values are present in the query string startDate = req.Query["startdate"]; @@ -47,7 +48,7 @@ public async Task Run( try { var result = await _handler.GetAverageCarbonIntensityAsync(location, DateTimeOffset.Parse(startDate), DateTimeOffset.Parse(endDate)); - log.LogInformation($"For location {location} Starting at: {startDate} Ending at: {endDate} the Average Emissions Rating is: {result}."); + _log.LogInformation($"For location {location} Starting at: {startDate} Ending at: {endDate} the Average Emissions Rating is: {result}."); return new OkObjectResult(result); } diff --git a/samples/azure/azure-function/GetForecast.cs b/samples/azure/azure-function/GetForecast.cs index 564a0138e..eb3536620 100644 --- a/samples/azure/azure-function/GetForecast.cs +++ b/samples/azure/azure-function/GetForecast.cs @@ -1,7 +1,7 @@ using GSF.CarbonAware.Handlers; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System; @@ -15,16 +15,17 @@ namespace CarbonAwareFunctions public class GetForecast { private readonly IForecastHandler _handler; + private readonly ILogger _log; - public GetForecast(IForecastHandler handler) + public GetForecast(IForecastHandler handler, ILogger log) { this._handler = handler; + this._log = log; } - [FunctionName("GetCurrentForecast")] + [Function("GetCurrentForecast")] public async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { //Get the startDate, endDate, location, and duration from the request query if the values are present in the query string startDate = req.Query["startdate"]; diff --git a/samples/azure/azure-function/Program.cs b/samples/azure/azure-function/Program.cs new file mode 100644 index 000000000..0f64de4d5 --- /dev/null +++ b/samples/azure/azure-function/Program.cs @@ -0,0 +1,24 @@ +using GSF.CarbonAware.Configuration; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using System.IO; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureAppConfiguration((context, builder) => { + var env = context.HostingEnvironment; + builder.AddJsonFile(Path.Combine(env.ContentRootPath, "appsettings.json"), optional: true, reloadOnChange: false) + .AddJsonFile(Path.Combine(env.ContentRootPath, $"appsettings.{env.EnvironmentName}.json"), optional: true, reloadOnChange: false) + .AddEnvironmentVariables(); + }) + .ConfigureServices((context,services) => { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + services.AddEmissionsServices(context.Configuration); + services.AddForecastServices(context.Configuration); + }) + .Build(); + +host.Run(); \ No newline at end of file diff --git a/samples/azure/azure-function/README.md b/samples/azure/azure-function/README.md index 0021630de..641919de4 100644 --- a/samples/azure/azure-function/README.md +++ b/samples/azure/azure-function/README.md @@ -19,29 +19,20 @@ will use. The Carbon Aware SDK is included in the function .csproj file by [creating and adding the SDK as a package](../../docs/packaging.md#included-scripts). -The [Startup.cs](./Startup.cs) file uses dependency injection to access the +The [Program.cs](./Program.cs) file uses dependency injection to access the handlers in the library. The following code initializes the C# Library: ```C# - public override void Configure(IFunctionsHostBuilder builder) - { - var configuration = builder.GetContext().Configuration; - builder.Services - .AddEmissionsServices(configuration) - .AddForecastServices(configuration); - } + // omitted + .ConfigureServices((context,services) => { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + services.AddEmissionsServices(context.Configuration); + services.AddForecastServices(context.Configuration); + }) + // omitted ``` -> Note as the in-process -> [Azure Function uses dependency injection](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection) -> though via -> [Microsoft.Azure.Functions.Extensions](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/) -> there is a version conflict of -> [Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration). -> It is fixed adding a version specific project dependency (in .csproj) to the -> same version as the Carbon Aware SDK. Microsoft.Extensions.Configuration is -> backwards compatible. - ## Run Function Locally Both Azure Function apps can be diff --git a/samples/azure/azure-function/Startup.cs b/samples/azure/azure-function/Startup.cs deleted file mode 100644 index 1660065ba..000000000 --- a/samples/azure/azure-function/Startup.cs +++ /dev/null @@ -1,31 +0,0 @@ -using GSF.CarbonAware.Configuration; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System.IO; - -[assembly: FunctionsStartup(typeof(CarbonAwareFunctions.Startup))] - -namespace CarbonAwareFunctions -{ - public class Startup : FunctionsStartup - { - public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) - { - FunctionsHostBuilderContext context = builder.GetContext(); - - builder.ConfigurationBuilder - .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false) - .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false) - .AddEnvironmentVariables(); - } - - public override void Configure(IFunctionsHostBuilder builder) - { - var configuration = builder.GetContext().Configuration; - builder.Services - .AddEmissionsServices(configuration) - .AddForecastServices(configuration); - } - } -} \ No newline at end of file diff --git a/samples/azure/azure-function/function.csproj b/samples/azure/azure-function/function.csproj index e48592f82..4dd9facd2 100644 --- a/samples/azure/azure-function/function.csproj +++ b/samples/azure/azure-function/function.csproj @@ -1,9 +1,16 @@ - net6.0 + net8.0 v4 + Exe + + + + + + @@ -11,8 +18,6 @@ - - @@ -26,4 +31,7 @@ PreserveNewest + + + diff --git a/samples/lib-integration/ConsoleApp/ConsoleApp.csproj b/samples/lib-integration/ConsoleApp/ConsoleApp.csproj index c688178ea..ffd7e1267 100644 --- a/samples/lib-integration/ConsoleApp/ConsoleApp.csproj +++ b/samples/lib-integration/ConsoleApp/ConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable diff --git a/src/CarbonAware.CLI/src/CarbonAware.CLI.csproj b/src/CarbonAware.CLI/src/CarbonAware.CLI.csproj index fcc1f9bfb..f097ae0c9 100644 --- a/src/CarbonAware.CLI/src/CarbonAware.CLI.csproj +++ b/src/CarbonAware.CLI/src/CarbonAware.CLI.csproj @@ -4,7 +4,7 @@ caw win-x64;osx-x64;linux-x64 Exe - net6.0 + net8.0 enable enable 34d82203-20b1-4fcd-9bd4-3b247f13bad7 diff --git a/src/CarbonAware.CLI/src/Dockerfile b/src/CarbonAware.CLI/src/Dockerfile index 94c08a68e..8bd94af86 100644 --- a/src/CarbonAware.CLI/src/Dockerfile +++ b/src/CarbonAware.CLI/src/Dockerfile @@ -1,5 +1,5 @@ -# Set the base image as the .NET 6.0 SDK (this includes the runtime) -FROM mcr.microsoft.com/dotnet/sdk:6.0 as build-env +# Set the base image as the .NET 8.0 SDK (this includes the runtime) +FROM mcr.microsoft.com/dotnet/sdk:8.0 as build-env # Copy everything and publish the release (publish implicitly restores and builds) COPY ./src/ ./ @@ -27,7 +27,7 @@ LABEL com.github.actions.icon="sliders" LABEL com.github.actions.color="purple" # Relayer the .NET SDK, anew with the build output -FROM mcr.microsoft.com/dotnet/runtime:6.0 +FROM mcr.microsoft.com/dotnet/runtime:8.0 COPY --from=build-env /out . RUN apt-get update && apt-get install jq -y diff --git a/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj b/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj index 9b0dd0efe..a79c9cdf0 100644 --- a/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj +++ b/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable false enable diff --git a/src/CarbonAware.CLI/test/unitTests/CarbonAware.CLI.UnitTests.csproj b/src/CarbonAware.CLI/test/unitTests/CarbonAware.CLI.UnitTests.csproj index a97048508..8c56dd8ba 100644 --- a/src/CarbonAware.CLI/test/unitTests/CarbonAware.CLI.UnitTests.csproj +++ b/src/CarbonAware.CLI/test/unitTests/CarbonAware.CLI.UnitTests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable false enable diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/CarbonAware.DataSources.ElectricityMaps.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/CarbonAware.DataSources.ElectricityMaps.Mocks.csproj index ac0346757..10277e544 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/CarbonAware.DataSources.ElectricityMaps.Mocks.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/mock/CarbonAware.DataSources.ElectricityMaps.Mocks.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable false diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj index d9dab5e3e..1d3c1ad32 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/CarbonAware.DataSources.ElectricityMaps.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable enable true diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/CarbonAware.DataSources.ElectricityMaps.Tests.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/CarbonAware.DataSources.ElectricityMaps.Tests.csproj index 34e343371..b726b4773 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/CarbonAware.DataSources.ElectricityMaps.Tests.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/test/CarbonAware.DataSources.ElectricityMaps.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable false diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj index 60a945672..a8878c6ae 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj index 8db8bc27b..310a6ed47 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable enable diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj index 742632c0e..0375049af 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable false diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/CarbonAware.DataSources.Json.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/CarbonAware.DataSources.Json.Mocks.csproj index a6f5e7724..898d7f162 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/CarbonAware.DataSources.Json.Mocks.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/CarbonAware.DataSources.Json.Mocks.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable false diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj index 7c9905d41..7bba6e61d 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable enable true diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj index 1addeb0dd..065ebeee5 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable false diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj index d66d5e8c7..40ecc3ffe 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable true diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs index 872ec4bae..7ceb5b012 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/Configuration/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using CarbonAware.DataSources.Json.Configuration; using CarbonAware.DataSources.WattTime.Configuration; using CarbonAware.Exceptions; +using CarbonAware.Proxies.Cache; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -48,6 +49,8 @@ public static IServiceCollection AddDataSourceService(this IServiceCollection se } } + services.SetupCacheForEmissionsDataSource(configuration); + switch (forecastDataSource) { case DataSourceType.JSON: @@ -96,4 +99,29 @@ private static DataSourceType GetDataSourceTypeFromValue(string? srcVal) } return result; } + + private static IServiceCollection SetupCacheForEmissionsDataSource(this IServiceCollection services, IConfiguration configuration) + { + var emissionsDataCache = configuration.EmissionsDataCache(); + if(emissionsDataCache.Enabled){ + var emissionsDataSourceDescriptor = services.SingleOrDefault(s => s.ServiceType == typeof(IEmissionsDataSource)); + var type = emissionsDataSourceDescriptor!.ImplementationType; + if(type == null) return services; + services.Replace + ( + ServiceDescriptor.Describe + ( + typeof(IEmissionsDataSource), + serviceProvider => + LatestEmissionsCache.CreateProxy + ( + (IEmissionsDataSource)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, type!), + emissionsDataCache + )!, + emissionsDataSourceDescriptor.Lifetime + ) + ); + } + return services; + } } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj index 1adda21a9..061c0cbab 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/mock/CarbonAware.DataSources.WattTime.Mocks.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable false diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj index 20f6d7230..996b31ae4 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/CarbonAware.DataSources.WattTime.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable true diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/CarbonAware.DataSources.WattTime.Tests.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/CarbonAware.DataSources.WattTime.Tests.csproj index 295fb782c..77f142861 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/CarbonAware.DataSources.WattTime.Tests.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/CarbonAware.DataSources.WattTime.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable false diff --git a/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj b/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj index d05da871e..50211d65d 100644 --- a/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj +++ b/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj @@ -5,7 +5,7 @@ - net6.0 + net8.0 enable enable true diff --git a/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj b/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj index 498ef5124..28dc668ef 100644 --- a/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj +++ b/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false enable diff --git a/src/CarbonAware.Tools/CarbonAware.Tools.AWSRegionTestDataGenerator/CarbonAware.Tools.AWSRegionTestDataGenerator.csproj b/src/CarbonAware.Tools/CarbonAware.Tools.AWSRegionTestDataGenerator/CarbonAware.Tools.AWSRegionTestDataGenerator.csproj index 203c50ed1..b5757b3b9 100644 --- a/src/CarbonAware.Tools/CarbonAware.Tools.AWSRegionTestDataGenerator/CarbonAware.Tools.AWSRegionTestDataGenerator.csproj +++ b/src/CarbonAware.Tools/CarbonAware.Tools.AWSRegionTestDataGenerator/CarbonAware.Tools.AWSRegionTestDataGenerator.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable false diff --git a/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/CarbonAware.Tools.AzureRegionTestDataGenerator.csproj b/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/CarbonAware.Tools.AzureRegionTestDataGenerator.csproj index dc24429ba..ddfd8938d 100644 --- a/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/CarbonAware.Tools.AzureRegionTestDataGenerator.csproj +++ b/src/CarbonAware.Tools/CarbonAware.Tools.AzureRegionTestDataGenerator/CarbonAware.Tools.AzureRegionTestDataGenerator.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable false diff --git a/src/CarbonAware.WebApi/src/.config/dotnet-tools.json b/src/CarbonAware.WebApi/src/.config/dotnet-tools.json index 1a1607fe5..9a07f6919 100644 --- a/src/CarbonAware.WebApi/src/.config/dotnet-tools.json +++ b/src/CarbonAware.WebApi/src/.config/dotnet-tools.json @@ -3,10 +3,10 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "6.2.3", + "version": "6.5.0", "commands": [ "swagger" ] } } -} \ No newline at end of file +} diff --git a/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj b/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj index 356e23088..de8904092 100644 --- a/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj +++ b/src/CarbonAware.WebApi/src/CarbonAware.WebApi.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable 8d822819-8a1f-45e4-95fb-d4a9c3a9439f @@ -15,13 +15,15 @@ + + - - - - - + + + + + @@ -45,4 +47,4 @@ - \ No newline at end of file + diff --git a/src/CarbonAware.WebApi/src/Configuration/CarbonExporterConfiguration.cs b/src/CarbonAware.WebApi/src/Configuration/CarbonExporterConfiguration.cs new file mode 100644 index 000000000..35f8a3bc1 --- /dev/null +++ b/src/CarbonAware.WebApi/src/Configuration/CarbonExporterConfiguration.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Configuration; + +namespace CarbonAware.WebApi.Configuration; + + +internal class CarbonExporterConfiguration +{ + public const string Key = "CarbonExporter"; + + public int PeriodInHours { get; set; } = 24; + + public void AssertValid() + { + if(PeriodInHours <= 0) + { + throw new ArgumentException($"The value of CarbonExporter.PeriodInHours must be greater than 0."); + } + } +} \ No newline at end of file diff --git a/src/CarbonAware.WebApi/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.WebApi/src/Configuration/ServiceCollectionExtensions.cs index 48820a8bb..0c1458b23 100644 --- a/src/CarbonAware.WebApi/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/CarbonAware.WebApi/src/Configuration/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Options; +using CarbonAware.WebApi.Metrics; +using OpenTelemetry.Metrics; namespace CarbonAware.WebApi.Configuration; @@ -30,6 +32,34 @@ public static void AddMonitoringAndTelemetry(this IServiceCollection services, I // Can be extended in the future to support a different provider like Zipkin, Prometheus etc } + + } + + public static IServiceCollection AddCarbonExporter(this IServiceCollection services, IConfiguration configuration) + { + var envVars = configuration?.GetSection(CarbonAwareVariablesConfiguration.Key).Get(); + var enableCarbonExporter = envVars?.EnableCarbonExporter ?? false; + if(enableCarbonExporter){ + var carbonExporter = configuration?.GetSection(CarbonExporterConfiguration.Key); + services.Configure(c => + { + carbonExporter?.Bind(c); + }); + + services.AddOpenTelemetry() + .WithMetrics(meterProviderBuilder => + meterProviderBuilder + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + }) + .ConfigureResource(rb => rb.AddDetector(sp => sp.GetRequiredService())) + .AddMeter(CarbonMetrics.MeterName) + .AddPrometheusExporter() + ); + } + return services; } private static bool IsAppInsightsConfigured(IConfiguration? configuration, ILogger logger) diff --git a/src/CarbonAware.WebApi/src/Dockerfile b/src/CarbonAware.WebApi/src/Dockerfile index bc46714fe..f01e27777 100644 --- a/src/CarbonAware.WebApi/src/Dockerfile +++ b/src/CarbonAware.WebApi/src/Dockerfile @@ -1,6 +1,7 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env WORKDIR /app +ENV DOTNET_ROLL_FORWARD LatestMajor # Copy everything from source COPY . ./ # Use implicit restore to build and publish @@ -12,7 +13,7 @@ RUN dotnet tool restore && \ dotnet tool run swagger tofile --output /app/publish/wwwroot/api/v1/swagger.yaml --yaml /app/publish/CarbonAware.WebApi.dll v1 # Build runtime image -FROM mcr.microsoft.com/dotnet/aspnet:6.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0 # Install curl for health check RUN apt-get update && \ apt-get install -y --no-install-recommends curl diff --git a/src/CarbonAware.WebApi/src/Metrics/CarbonMetrics.cs b/src/CarbonAware.WebApi/src/Metrics/CarbonMetrics.cs new file mode 100644 index 000000000..7efc244a1 --- /dev/null +++ b/src/CarbonAware.WebApi/src/Metrics/CarbonMetrics.cs @@ -0,0 +1,93 @@ +using CarbonAware.WebApi.Configuration; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Options; +using GSF.CarbonAware.Handlers; +using GSF.CarbonAware.Models; + +namespace CarbonAware.WebApi.Metrics; + +internal class CarbonMetrics : IDisposable +{ + private readonly ILogger _logger; + + private readonly IOptionsMonitor _configurationMonitor; + private CarbonExporterConfiguration _configuration => this._configurationMonitor.CurrentValue; + internal const string MeterName = "Carbon.Aware.Metric"; + internal const string ActivitySourceName = "Carbon.Aware.Metric"; + internal const string GaugeName = "carbon.aware.intensity"; + + public ActivitySource ActivitySource { get; } + + private readonly IEmissionsHandler _emissionsHandler; + private readonly ILocationHandler _locationHandler; + + private readonly Meter _meter; + + private readonly ConcurrentBag _locations = new ConcurrentBag(); + private readonly IDictionary> _gauges = new ConcurrentDictionary>(); + + public CarbonMetrics(IMeterFactory meterFactory, IOptionsMonitor monitor, ILogger logger, IEmissionsHandler emissionsHandler, ILocationHandler locationHandler) + { + _emissionsHandler = emissionsHandler ?? throw new ArgumentNullException(nameof(emissionsHandler)); + _locationHandler = locationHandler ?? throw new ArgumentNullException(nameof(locationHandler)); + _configurationMonitor = monitor; + _logger = logger; + _configuration.AssertValid(); + + string? version = typeof(CarbonMetrics).Assembly.GetName().Version?.ToString(); + ActivitySource = new ActivitySource(ActivitySourceName, version); + _meter = meterFactory.Create(MeterName, version); + InitLocations(); + } + + private void InitLocations() + { + // initialize locations and guages + _locations.Clear(); + _gauges.Clear(); + + // load locations + Task> locationsTask = _locationHandler.GetLocationsAsync(); + try + { + locationsTask.Result.Keys.ToList().ForEach(d => _locations.Add(d)); + // create guages for each locaton + foreach(var loc in _locations){ + _gauges[loc] = _meter.CreateObservableGauge(CarbonMetrics.GaugeName, () => GetIntensity(loc)); + } + } + catch(Exception ex) + { + _logger.LogWarning(ex.Message); + _locations.Clear(); + _gauges.Clear(); + } + } + + public void Dispose() + { + _meter.Dispose(); + ActivitySource.Dispose(); + } + + private Measurement GetIntensity(string location){ + try + { + var end = DateTimeOffset.UtcNow; + var start = end.AddHours(-1*_configuration.PeriodInHours); + var intensity = _emissionsHandler.GetEmissionsDataAsync(location, start, end) + .Result + .MaxBy(d => d.Time)! + .Rating; + var measurement = new Measurement(intensity, new TagList(){{"location", location}}); + return measurement; + } + catch(Exception ex) + { + _logger.LogWarning(ex.Message); + return new Measurement(0, new TagList(){{"location", location}}); + } + } +} \ No newline at end of file diff --git a/src/CarbonAware.WebApi/src/Metrics/MetricsResourceDetector.cs b/src/CarbonAware.WebApi/src/Metrics/MetricsResourceDetector.cs new file mode 100644 index 000000000..0f8398c09 --- /dev/null +++ b/src/CarbonAware.WebApi/src/Metrics/MetricsResourceDetector.cs @@ -0,0 +1,17 @@ +using CarbonAware.WebApi.Metrics; +using OpenTelemetry.Resources; + +internal class MetricsResourceDetector : IResourceDetector +{ + private readonly CarbonMetrics _carbonMetrics; + + public MetricsResourceDetector(CarbonMetrics carbonMetrics) + { + _carbonMetrics = carbonMetrics; + } + + public Resource Detect() + { + return ResourceBuilder.CreateEmpty().Build(); + } +} \ No newline at end of file diff --git a/src/CarbonAware.WebApi/src/Program.cs b/src/CarbonAware.WebApi/src/Program.cs index 9a70c15ab..b027ad12f 100644 --- a/src/CarbonAware.WebApi/src/Program.cs +++ b/src/CarbonAware.WebApi/src/Program.cs @@ -7,6 +7,7 @@ using Microsoft.OpenApi.Models; using OpenTelemetry.Resources; using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; using Swashbuckle.AspNetCore.SwaggerGen; using System.Reflection; @@ -16,17 +17,16 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddOpenTelemetryTracing(tracerProviderBuilder => -{ - tracerProviderBuilder +builder.Services.AddOpenTelemetry() + .WithTracing(tracerProviderBuilder => + tracerProviderBuilder .AddConsoleExporter() .AddSource(serviceName) .SetResourceBuilder( ResourceBuilder.CreateDefault() .AddService(serviceName: serviceName, serviceVersion: serviceVersion)) .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation(); -}); + .AddAspNetCoreInstrumentation()); // Add services to the container. builder.Services.AddControllers(options => @@ -70,6 +70,8 @@ builder.Services.AddMonitoringAndTelemetry(builder.Configuration); +builder.Services.AddCarbonExporter(builder.Configuration); + builder.Services.AddSwaggerGen(c => { c.MapType(() => new OpenApiSchema { Type = "string", Format = "time-span" }); }); @@ -112,9 +114,14 @@ app.MapHealthChecks("/health"); +var enableCarbonExporter = config?.EnableCarbonExporter ?? false; +if(enableCarbonExporter){ + app.UseOpenTelemetryPrometheusScrapingEndpoint(); +} + app.Run(); -// Please view https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory +// Please view https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0#basic-tests-with-the-default-webapplicationfactory // This line is needed to allow for Integration Testing public partial class Program { } diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj b/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj index df1ed5ada..fd9179089 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable false enable diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs index d4954bbd6..975d00d04 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs @@ -20,6 +20,7 @@ class CarbonAwareControllerTests : IntegrationTestingBase { private readonly string healthURI = "/health"; private readonly string fakeURI = "/fake-endpoint"; + private readonly string metricsURI = "/metrics"; private readonly string bestLocationsURI = "/emissions/bylocations/best"; private readonly string bylocationsURI = "/emissions/bylocations"; private readonly string bylocationURI = "/emissions/bylocation"; @@ -47,6 +48,16 @@ public async Task FakeEndPoint_ReturnsNotFound() Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } + [Test] + public async Task MetricsEndPoint_ReturnsOK() + { + //Use client to get endpoint + var result = await _client.GetAsync(metricsURI); + Assert.That(result, Is.Not.Null); + Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + } + + //ISO8601: YYYY-MM-DD [TestCase("2022-1-1T04:05:06Z", "2022-1-2T04:05:06Z", "eastus", nameof(ByLocationURI_ReturnsOK) + "0")] [TestCase("2021-12-25T00:00:00+06:00", "2021-12-26T00:00:00+06:00", "westus", nameof(ByLocationURI_ReturnsOK) + "1")] diff --git a/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs b/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs index 5dd7310b5..8d6d1c46c 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/IntegrationTestingBase.cs @@ -21,6 +21,7 @@ internal abstract class IntegrationTestingBase internal DataSourceType _dataSource; internal string? _emissionsDataSourceEnv; internal string? _forecastDataSourceEnv; + internal string? _enableCarbonExporterEnv; internal WebApplicationFactory _factory; protected HttpClient _client; internal IDataSourceMocker _dataSourceMocker; @@ -33,6 +34,7 @@ public IntegrationTestingBase(DataSourceType dataSource) _dataSource = dataSource; _emissionsDataSourceEnv = null; _forecastDataSourceEnv = null; + _enableCarbonExporterEnv = null; _factory = new WebApplicationFactory(); } @@ -77,6 +79,8 @@ public void Setup() { _emissionsDataSourceEnv = Environment.GetEnvironmentVariable("DataSources__EmissionsDataSource"); _forecastDataSourceEnv = Environment.GetEnvironmentVariable("DataSources__ForecastDataSource"); + _enableCarbonExporterEnv = Environment.GetEnvironmentVariable("CarbonAwareVars__EnableCarbonExporter"); + Environment.SetEnvironmentVariable("CarbonAwareVars__EnableCarbonExporter", "true"); //Switch between different data sources as needed //Each datasource should have an accompanying DataSourceMocker that will perform setup activities switch (_dataSource) @@ -153,5 +157,6 @@ public void TearDown() _dataSourceMocker?.Dispose(); Environment.SetEnvironmentVariable("DataSources__EmissionsDataSource", _emissionsDataSourceEnv); Environment.SetEnvironmentVariable("DataSources__ForecastDataSource", _forecastDataSourceEnv); + Environment.SetEnvironmentVariable("CarbonAwareVars__EnableCarbonMetrics", _enableCarbonExporterEnv); } } \ No newline at end of file diff --git a/src/CarbonAware.WebApi/test/unitTests/CarbonAware.WebApi.UnitTests.csproj b/src/CarbonAware.WebApi/test/unitTests/CarbonAware.WebApi.UnitTests.csproj index a1859042d..f630cd0f8 100644 --- a/src/CarbonAware.WebApi/test/unitTests/CarbonAware.WebApi.UnitTests.csproj +++ b/src/CarbonAware.WebApi/test/unitTests/CarbonAware.WebApi.UnitTests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable false enable diff --git a/src/CarbonAware.WebApi/test/unitTests/Configuration/CarbonExporterConfigurationTests.cs b/src/CarbonAware.WebApi/test/unitTests/Configuration/CarbonExporterConfigurationTests.cs new file mode 100644 index 000000000..af29280f0 --- /dev/null +++ b/src/CarbonAware.WebApi/test/unitTests/Configuration/CarbonExporterConfigurationTests.cs @@ -0,0 +1,30 @@ +using CarbonAware.WebApi.Configuration; +using NUnit.Framework; + +namespace CarbonAware.WepApi.UnitTests; + +class CarbonExporterConfigurationTests +{ + [TestCase(12, TestName = "AssertValid: PeriodInHours greater than 0")] + [TestCase(1, TestName = "AssertValid: PeriodInHours equals to 1")] + public void AssertValid_PeriodInHoursGreaterThanZero_DoesNotThrowException(int periodInHours) + { + CarbonExporterConfiguration carbonExporterConfiguration = new CarbonExporterConfiguration() + { + PeriodInHours = periodInHours + }; + + Assert.DoesNotThrow(() => carbonExporterConfiguration.AssertValid()); + } + + [TestCase(0, TestName = "AssertValid: PeriodInHours equals to 0")] + public void AssertValid_PeriodInHoursGreaterThanZero_ThrowException(int periodInHours) + { + CarbonExporterConfiguration carbonExporterConfiguration = new CarbonExporterConfiguration() + { + PeriodInHours = periodInHours + }; + + Assert.Throws(() => carbonExporterConfiguration.AssertValid()); + } +} \ No newline at end of file diff --git a/src/CarbonAware.WebApi/test/unitTests/Configuration/ServiceCollectionExtensionsTests.cs b/src/CarbonAware.WebApi/test/unitTests/Configuration/ServiceCollectionExtensionsTests.cs index 2e8edd87c..f675160a2 100644 --- a/src/CarbonAware.WebApi/test/unitTests/Configuration/ServiceCollectionExtensionsTests.cs +++ b/src/CarbonAware.WebApi/test/unitTests/Configuration/ServiceCollectionExtensionsTests.cs @@ -82,6 +82,61 @@ public void AddMonitoringAndTelemetry_DoesNotAddServices_WithoutTelemetryProvide Assert.That(services.Count, Is.EqualTo(0)); } + [Test] + public void AddCarbonExporter_AddsServices_IsEnabledInConfiguration() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + { "CarbonAwareVars:EnableCarbonExporter", "true" } + }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act & Assert + Assert.DoesNotThrow(() => services.AddCarbonExporter(configuration)); + Assert.That(services.Count, Is.GreaterThan(0)); + } + + [Test] + public void AddCarbonExporter_DoesNotAddServices_IsDisabledInConfiguration() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + { "CarbonAwareVars:EnableCarbonExporter", "false" } + }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act & Assert + Assert.DoesNotThrow(() => services.AddCarbonExporter(configuration)); + Assert.That(services.Count, Is.EqualTo(0)); + } + + + [Test] + public void AddCarbonExporter_DoesNotAddServices_WithoutConfiguration() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary{}; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + // Act & Assert + Assert.DoesNotThrow(() => services.AddCarbonExporter(configuration)); + Assert.That(services.Count, Is.EqualTo(0)); + } + [Test] public void CreateConsoleLogger_ReturnsILogger() { diff --git a/src/CarbonAware/src/CarbonAware.csproj b/src/CarbonAware/src/CarbonAware.csproj index d691e0f80..d04ce3a78 100644 --- a/src/CarbonAware/src/CarbonAware.csproj +++ b/src/CarbonAware/src/CarbonAware.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable true diff --git a/src/CarbonAware/src/CarbonAwareVariablesConfiguration.cs b/src/CarbonAware/src/CarbonAwareVariablesConfiguration.cs index 13eea1dcb..ae37c78aa 100644 --- a/src/CarbonAware/src/CarbonAwareVariablesConfiguration.cs +++ b/src/CarbonAware/src/CarbonAwareVariablesConfiguration.cs @@ -36,6 +36,8 @@ internal class CarbonAwareVariablesConfiguration public string TelemetryProvider { get; set; } + public Boolean EnableCarbonExporter { get;set; } + public Boolean VerboseApi {get; set;} } diff --git a/src/CarbonAware/src/Configuration/EmissionsDataCacheConfiguration.cs b/src/CarbonAware/src/Configuration/EmissionsDataCacheConfiguration.cs new file mode 100644 index 000000000..9eec7a89b --- /dev/null +++ b/src/CarbonAware/src/Configuration/EmissionsDataCacheConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; + +namespace CarbonAware.Configuration; + +internal class EmissionsDataCacheConfiguration +{ + public const string Key = "EmissionsDataCache"; + + public bool Enabled { get; set; } = false; + + public int ExpirationMin { get; set; } = 0; + + public void AssertValid() + { + if(Enabled & ExpirationMin <= 0) + { + throw new ArgumentException($"Expiration period for data cache value must be greater than 0."); + } + } +} \ No newline at end of file diff --git a/src/CarbonAware/src/Configuration/EmissionsDataCacheConfigurationExtensions.cs b/src/CarbonAware/src/Configuration/EmissionsDataCacheConfigurationExtensions.cs new file mode 100644 index 000000000..76e1ed345 --- /dev/null +++ b/src/CarbonAware/src/Configuration/EmissionsDataCacheConfigurationExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Configuration; + +namespace CarbonAware.Configuration; + +internal static class EmissionsDataCacheConfigurationExtensions +{ + public static EmissionsDataCacheConfiguration EmissionsDataCache(this IConfiguration configuration) + { + var dataCache = configuration.GetSection(EmissionsDataCacheConfiguration.Key).Get() ?? new EmissionsDataCacheConfiguration(); + dataCache.AssertValid(); + + return dataCache; + } +} \ No newline at end of file diff --git a/src/CarbonAware/src/Proxies/Cache/LatestEmissionsCache.cs b/src/CarbonAware/src/Proxies/Cache/LatestEmissionsCache.cs new file mode 100644 index 000000000..116f44db6 --- /dev/null +++ b/src/CarbonAware/src/Proxies/Cache/LatestEmissionsCache.cs @@ -0,0 +1,150 @@ +using CarbonAware.Configuration; +using CarbonAware.Interfaces; +using System.Collections; +using System.Collections.Concurrent; +using System.Reflection; + +namespace CarbonAware.Proxies.Cache; + +/// +/// A proxy class for IEmissionsDataSource to cache EmissionsData. +/// This class caches data which is queried latestly for each Location. +/// The cached value are used if it satisfies all of the conditions listed below. +/// +/// +/// it is not exceeded the expiration period +/// +/// +/// the name of the location is match with the query +/// +/// +/// the time is match with the query +/// +/// +/// +/// The target class of the proxy +class LatestEmissionsCache : DispatchProxy where T : class, IEmissionsDataSource +{ + + private IEmissionsDataSource? _target { get; set; } + + readonly private ConcurrentDictionary)> _cache = + new ConcurrentDictionary)>(); + + private EmissionsDataCacheConfiguration? _config { get; set; } + + public static T? CreateProxy(T target, EmissionsDataCacheConfiguration config) + { + var proxy = Create>() as LatestEmissionsCache; + proxy!._target = target; + proxy._config = config; + return proxy as T; + } + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if(targetMethod!.Name.Equals("GetCarbonIntensityAsync") && args![0]!.GetType() == typeof(Location)) + { + var location = (Location?)args[0]; + var start = (DateTimeOffset)args[1]!; + var end = (DateTimeOffset)args[2]!; + return ProxyGetCarbonIntensityAsync(targetMethod, location!, start, end); + } else if(targetMethod.Name.Equals("GetCarbonIntensityAsync") && args![0]!.GetType().GetInterfaces().Contains(typeof(IEnumerable))) + { + var locations = (IEnumerable?)args[0]; + var start = (DateTimeOffset)args[1]!; + var end = (DateTimeOffset)args[2]!; + return ProxyGetCarbonIntensityAsync(targetMethod, locations!, start, end); + } + return targetMethod.Invoke(_target, args); + } + + private Task> ProxyGetCarbonIntensityAsync(MethodInfo? original, Location location, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) + { + var cachedData = GetCachedData(location, periodStartTime, periodEndTime); + if(cachedData.Count() != 0) + { + return Task.FromResult(cachedData); + } + + object[] args = {location, periodStartTime, periodEndTime}; + var resultFromOriginal = (Task>?)original!.Invoke(_target, args); + + if(string.IsNullOrEmpty(location.Name)) + { + return resultFromOriginal!; + } else + { + var expiration = DateTimeOffset.UtcNow.AddMinutes(_config!.ExpirationMin); + var result = resultFromOriginal!.ContinueWith + ( + c => + { + _cache.AddOrUpdate(location.Name, (expiration, c.Result), (_, _) => (expiration, c.Result)); + return c.Result; + } + ); + return result; + } + } + + private Task> ProxyGetCarbonIntensityAsync(MethodInfo? original, IEnumerable locations, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) + { + var cachedData = Enumerable.Empty(); + var useCacheFor = Enumerable.Empty(); + foreach (var location in locations) + { + var cachedDataForLocation = GetCachedData(location, periodStartTime, periodEndTime); + if(cachedDataForLocation.Count() != 0) + { + cachedData = cachedData.Union(cachedDataForLocation); + useCacheFor = useCacheFor.Append(location.Name); + } + } + + var locationsForQuery = locations.Where(l => string.IsNullOrEmpty(l.Name) || !useCacheFor.Contains(l.Name)); + if(locationsForQuery.Count() == 0){ + return Task.FromResult(cachedData); + } + + object[] args = {locationsForQuery, periodStartTime, periodEndTime}; + var resultFromOriginal = (Task>?)original!.Invoke(_target, args); + + var expiration = DateTimeOffset.UtcNow.AddMinutes(_config!.ExpirationMin); + var result = resultFromOriginal!.ContinueWith + ( + c => + { + foreach (var location in locationsForQuery) + { + if(!string.IsNullOrEmpty(location.Name)) + { + var data = c.Result.Where(d => d.Location.Equals(location.Name)); + _cache.AddOrUpdate(location.Name, (expiration, data), (_, _) => (expiration, data)); + } + } + return c.Result.Union(cachedData); + } + ); + return result; + } + + private IEnumerable GetCachedData(Location location, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) + { + if(!string.IsNullOrEmpty(location.Name) && _cache.ContainsKey(location.Name)) + { + var cachedValue = _cache.GetValueOrDefault(location.Name); + + // check expiration + if(cachedValue.Item1.CompareTo(DateTimeOffset.UtcNow) > 0) + { + IEnumerable emissions = cachedValue.Item2.Where(d => d.TimeBetween(periodStartTime, periodEndTime)); + if(emissions.Count() != 0) + { + return emissions; + } + } + } + return Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/src/CarbonAware/test/CarbonAware.Tests.csproj b/src/CarbonAware/test/CarbonAware.Tests.csproj index c11002e82..39071e6e9 100644 --- a/src/CarbonAware/test/CarbonAware.Tests.csproj +++ b/src/CarbonAware/test/CarbonAware.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable false @@ -13,6 +13,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CarbonAware/test/Configuration/EmissionsDataCacheConfigurationTests.cs b/src/CarbonAware/test/Configuration/EmissionsDataCacheConfigurationTests.cs new file mode 100644 index 000000000..5480204de --- /dev/null +++ b/src/CarbonAware/test/Configuration/EmissionsDataCacheConfigurationTests.cs @@ -0,0 +1,31 @@ +using CarbonAware.Configuration; + +namespace CarbonAware.Tests.Configuration; + +class EmissionsDataCacheConfigurationTests +{ + [TestCase(10, TestName = "AssertValid: ExpirationMin greater than 0")] + public void AssertValid_ExpirationMinGreaterThanOrEqualsToZero_DoesNotThrowException(int expirationMin) + { + EmissionsDataCacheConfiguration emissionsDataCacheConfig = new EmissionsDataCacheConfiguration() + { + Enabled = true, + ExpirationMin = expirationMin + }; + + Assert.DoesNotThrow(() => emissionsDataCacheConfig.AssertValid()); + } + + [TestCase(0, TestName = "AssertValid: ExpirationMin equals to 0")] + [TestCase(-10, TestName = "AssertValid: ExpirationMin less than 0")] + public void AssertValid_ExpirationMinLessThanZero_ThrowException(int expirationMin) + { + EmissionsDataCacheConfiguration emissionsDataCacheConfig = new EmissionsDataCacheConfiguration() + { + Enabled = true, + ExpirationMin = expirationMin + }; + + Assert.Throws(() => emissionsDataCacheConfig.AssertValid()); + } +} diff --git a/src/CarbonAware/test/Proxy/Cache/LatestEmissionsCacheTests.cs b/src/CarbonAware/test/Proxy/Cache/LatestEmissionsCacheTests.cs new file mode 100644 index 000000000..cd806e91e --- /dev/null +++ b/src/CarbonAware/test/Proxy/Cache/LatestEmissionsCacheTests.cs @@ -0,0 +1,231 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using CarbonAware.Configuration; +using CarbonAware.Interfaces; +using CarbonAware.Model; +using Moq; + +namespace CarbonAware.Proxies.Cache; + +class LatestEmissionsCacheTests +{ + + Mock? _mock; + + IEmissionsDataSource? _dataSource; + + IDictionary? _dateTimes; + + IDictionary? _emissions; + + IDictionary? _locations; + + [Test] + public void GetCarbonIntensityAsync_ForSingleLocation_UseCachedDataForSameQuery() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultFromDataSource = dataSource!.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + var resultFromCache = dataSource!.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + + // The results are same + Assert.AreEqual(resultFromDataSource, resultFromCache); + // The access to the data source occurs just once + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Once); + } + + [Test] + public void GetCarbonIntensityAsync_ForSingleLocation_UsePartOfCachedData() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultFromDataSource = dataSource!.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["thirdDay"]).Result; + var resultFromCache = dataSource!.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + + // The result for the second query is a part of the first one + Assert.AreEqual(resultFromDataSource.Count(), 2); + Assert.AreEqual(resultFromCache.Count(), 1); + // The second query does not access to tha data source + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Never); + } + + [Test] + public void GetCarbonIntensityAsync_ForSingleLocation_AccessToDataSourceIfCachedDataIsNotMached() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultOfFirst = dataSource!.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + var resultOfSecond = dataSource!.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["secondDay"], _dateTimes["thirdDay"]).Result; + + Assert.Contains(_emissions!["eastus-firstDay"], resultOfFirst.ToList()); + Assert.Contains(_emissions["eastus-secondDay"], resultOfSecond.ToList()); + // Both queries access to tha data source + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Once); + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(_locations!["eastus"], _dateTimes!["secondDay"], _dateTimes["thirdDay"]), Moq.Times.Once); + } + + [Test] + public void GetCarbonIntensityAsync_ForSingleLocation_AccessToDataSourceIfLocationNameIsEmpty() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultOfFirst = dataSource!.GetCarbonIntensityAsync(_locations!["coordinate"], _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + var resultOfSecond = dataSource!.GetCarbonIntensityAsync(_locations!["coordinate"], _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + + Assert.AreEqual(resultOfFirst, resultOfSecond); + // Both queries access to tha data source + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(_locations!["coordinate"], _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Exactly(2)); + } + + [Test] + public void GetCarbonIntensityAsync_ForMultiLocation_UseCachedDataForSameQuery() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultFromDataSource = dataSource!.GetCarbonIntensityAsync(new List{_locations!["eastus"], _locations["westus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + var resultFromCache = dataSource!.GetCarbonIntensityAsync(new List{_locations!["eastus"], _locations["westus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + + // The results are same + Assert.AreEqual(resultFromDataSource, resultFromCache); + // The access to the data source occurs just once + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(new List{_locations!["eastus"], _locations["westus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Once); + } + + [Test] + public void GetCarbonIntensityAsync_ForMultiLocation_UsePartOfCachedData() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultFromDataSource = dataSource!.GetCarbonIntensityAsync(new List{_locations!["eastus"], _locations["westus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + var resultFromCache = dataSource!.GetCarbonIntensityAsync(new List{_locations!["eastus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + + // The result for the second query is a part of the first one + Assert.AreEqual(resultFromDataSource.Count(), 2); + Assert.AreEqual(resultFromCache.Count(), 1); + // The access to the data source occurs just once + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(new List{_locations!["eastus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Never); + } + + [Test] + public void GetCarbonIntensityAsync_ForMultiLocation_UsePartOfCachedData2() + { + var config = new EmissionsDataCacheConfiguration(); + config.Enabled = true; + config.ExpirationMin = 10; + + var dataSource = LatestEmissionsCache.CreateProxy(_dataSource!, config); + var resultForFirst = dataSource!.GetCarbonIntensityAsync(new List{_locations!["eastus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + var resultForSecond = dataSource!.GetCarbonIntensityAsync(new List{_locations!["eastus"], _locations["westus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]).Result; + + Assert.AreEqual(resultForFirst.Count(), 1); + Assert.AreEqual(resultForSecond.Count(), 2); + // the first query + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(new List{_locations!["eastus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Once); + // the second query does not contain "eastus" because the data for "eastus" have been already cached + _mock!.Verify(ds => ds.GetCarbonIntensityAsync(new List{_locations!["westus"]}, _dateTimes!["firstDay"], _dateTimes["secondDay"]), Moq.Times.Once); + } + + [SetUp] + public void Setup() + { + _mock!.Invocations.Clear(); + } + + [OneTimeSetUp] + public void SetupDataAndMock() + { + // Crate test data + _dateTimes = new Dictionary + { + {"firstDay", new DateTimeOffset(2021, 11, 16, 0, 0, 0, TimeSpan.Zero)}, + {"secondDay", new DateTimeOffset(2021, 11, 17, 0, 0, 0, TimeSpan.Zero)}, + {"thirdDay", new DateTimeOffset(2021, 11, 18, 0, 0, 0, TimeSpan.Zero)} + }; + + var location1 = new Location(); + location1.Name = "eastus"; + var location2 = new Location(); + location2.Name = "westus"; + var location3 = new Location(); + location3.Latitude = 0; + location3.Longitude = 0; + _locations = new Dictionary + { + {"eastus", location1}, + {"westus", location2}, + {"coordinate", location3} + }; + + var emissions1 = new EmissionsData(); + emissions1.Location = "eastus"; + emissions1.Time = new DateTimeOffset(2021, 11, 16, 0, 55, 0, TimeSpan.Zero); + var emissions2 = new EmissionsData(); + emissions2.Location = "eastus"; + emissions2.Time = new DateTimeOffset(2021, 11, 17, 0, 55, 0, TimeSpan.Zero); + var emissions3 = new EmissionsData(); + emissions3.Location = "westus"; + emissions3.Time = new DateTimeOffset(2021, 11, 16, 0, 55, 0, TimeSpan.Zero); + var emissions4 = new EmissionsData(); + emissions4.Location = ""; + emissions4.Time = new DateTimeOffset(2021, 11, 16, 0, 55, 0, TimeSpan.Zero); + _emissions = new Dictionary + { + {"eastus-firstDay", emissions1}, + {"eastus-secondDay", emissions2}, + {"westus-firstDay", emissions3}, + {"coordinate-firstDay", emissions4} + }; + + // setup a mock + _mock = new Mock(); + + // setup for GetCarbonIntensityAsync(Location, DateTimeOffset, DateTimeOffset) + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(_locations["eastus"], _dateTimes["firstDay"], _dateTimes["secondDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["eastus-firstDay"]})); + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(_locations["eastus"], _dateTimes["secondDay"], _dateTimes["thirdDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["eastus-secondDay"]})); + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(_locations["eastus"], _dateTimes["firstDay"], _dateTimes["thirdDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["eastus-firstDay"], _emissions["eastus-secondDay"]})); + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(_locations["westus"], _dateTimes["firstDay"], _dateTimes["secondDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["westus-firstDay"]})); + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(_locations["coordinate"], _dateTimes["firstDay"], _dateTimes["secondDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["coordinate-firstDay"]})); + + // setup for GetCarbonIntensityAsync(IEnumerable, DateTimeOffset, DateTimeOffset) + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(new List{_locations["eastus"], _locations["westus"]}, _dateTimes["firstDay"], _dateTimes["secondDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["eastus-firstDay"], _emissions["westus-firstDay"]})); + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(new List{_locations["eastus"]}, _dateTimes["firstDay"], _dateTimes["secondDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["eastus-firstDay"]})); + _mock.Setup(ds => + ds.GetCarbonIntensityAsync(new List{_locations["westus"]}, _dateTimes["firstDay"], _dateTimes["secondDay"])) + .Returns(Task.FromResult((IEnumerable)new List{_emissions["westus-firstDay"]})); + + _dataSource = _mock.Object; + } + +} diff --git a/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs b/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs index 459351ae6..440e657b4 100644 --- a/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/GSF.CarbonAware/src/Configuration/ServiceCollectionExtensions.cs @@ -11,15 +11,22 @@ namespace GSF.CarbonAware.Configuration; public static class ServiceCollectionExtensions { - /// - /// Add services needed in order to use an Emissions service. - /// - public static IServiceCollection AddEmissionsServices(this IServiceCollection services, IConfiguration configuration) + + private static IServiceCollection ConfigureLocationDataSourcesConfiguration(this IServiceCollection services, IConfiguration configuration) { services.Configure(c => { configuration.GetSection(LocationDataSourcesConfiguration.Key).Bind(c); }); + return services; + } + + /// + /// Add services needed in order to use an Emissions service. + /// + public static IServiceCollection AddEmissionsServices(this IServiceCollection services, IConfiguration configuration) + { + services.ConfigureLocationDataSourcesConfiguration(configuration); services.TryAddSingleton(); services.AddDataSourceService(configuration); services.TryAddSingleton(); @@ -32,10 +39,7 @@ public static IServiceCollection AddEmissionsServices(this IServiceCollection se /// public static IServiceCollection AddForecastServices(this IServiceCollection services, IConfiguration configuration) { - services.Configure(c => - { - configuration.GetSection(LocationDataSourcesConfiguration.Key).Bind(c); - }); + services.ConfigureLocationDataSourcesConfiguration(configuration); services.TryAddSingleton(); services.AddDataSourceService(configuration); services.TryAddSingleton(); diff --git a/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj b/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj index f113e0b98..d49c11307 100644 --- a/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj +++ b/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable true @@ -48,13 +48,13 @@ - + - + \ No newline at end of file diff --git a/src/GSF.CarbonAware/src/GSF.CarbonAware.targets b/src/GSF.CarbonAware/src/GSF.CarbonAware.targets index 0e9a1ace2..752a668a6 100644 --- a/src/GSF.CarbonAware/src/GSF.CarbonAware.targets +++ b/src/GSF.CarbonAware/src/GSF.CarbonAware.targets @@ -16,7 +16,7 @@ - + diff --git a/src/GSF.CarbonAware/test/GSF.CarbonAware.Tests.csproj b/src/GSF.CarbonAware/test/GSF.CarbonAware.Tests.csproj index 64e74590c..696005074 100644 --- a/src/GSF.CarbonAware/test/GSF.CarbonAware.Tests.csproj +++ b/src/GSF.CarbonAware/test/GSF.CarbonAware.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable false diff --git a/src/clients/docker-generate-clients.sh b/src/clients/docker-generate-clients.sh index 142a64179..23478f2d4 100755 --- a/src/clients/docker-generate-clients.sh +++ b/src/clients/docker-generate-clients.sh @@ -45,7 +45,7 @@ docker run --rm \ -i http://$1/swagger/v1/swagger.json \ -g csharp-netcore \ -o /local/csharp \ - --additional-properties=targetFramework=net6.0 + --additional-properties=targetFramework=net8.0 # golang docker run --rm \ diff --git a/src/clients/generate-clients.sh b/src/clients/generate-clients.sh index 529d78e7a..9677589b8 100755 --- a/src/clients/generate-clients.sh +++ b/src/clients/generate-clients.sh @@ -12,5 +12,5 @@ cd generated openapi-generator-cli generate -i http://$1/swagger/v1/swagger.json -g java -o ./java openapi-generator-cli generate -i http://$1/swagger/v1/swagger.json -g python -o ./python openapi-generator-cli generate -i http://$1/swagger/v1/swagger.json -g javascript -o ./javascript -openapi-generator-cli generate -i http://$1/swagger/v1/swagger.json -g csharp-netcore -o ./csharp --additional-properties=targetFramework=net6.0 +openapi-generator-cli generate -i http://$1/swagger/v1/swagger.json -g csharp-netcore -o ./csharp --additional-properties=targetFramework=net8.0 openapi-generator-cli generate -i http://$1/swagger/v1/swagger.json -g go -o ./golang diff --git a/src/clients/tests/csharp/Dockerfile b/src/clients/tests/csharp/Dockerfile index 56b1cffcd..4bb8ffe61 100644 --- a/src/clients/tests/csharp/Dockerfile +++ b/src/clients/tests/csharp/Dockerfile @@ -1,5 +1,5 @@ # https://hub.docker.com/_/microsoft-dotnet -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build # copy csharp client WORKDIR /source/clients/csharp/src/Org.OpenAPITools @@ -13,7 +13,7 @@ COPY tests/csharp . RUN dotnet publish -c release -o /app # final stage/image -FROM mcr.microsoft.com/dotnet/aspnet:6.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app COPY tests/temp.env /.env COPY --from=build /app ./ diff --git a/src/clients/tests/csharp/csharp.csproj b/src/clients/tests/csharp/csharp.csproj index 0976a8879..f620d14f3 100644 --- a/src/clients/tests/csharp/csharp.csproj +++ b/src/clients/tests/csharp/csharp.csproj @@ -1,15 +1,15 @@ - net6.0 + net8.0 - - + +