diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 517732e79..000000000 --- a/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -# Generated dynamically using a template to support environment variables -src/ServicePulse.Host/app/js/app.constants.js \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ef9255cb..0f4a87196 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4.0.3 with: - node-version: 21.6.x + node-version: 21.6.x - name: Build Frontend run: .\build.ps1 working-directory: src/ServicePulse.Host @@ -38,7 +38,7 @@ jobs: - name: Build run: dotnet build src --configuration Release - name: Run .NET tests - uses: Particular/run-tests-action@v1.7.0 + uses: Particular/run-tests-action@v1.7.0 # Upload assets and packages - name: Upload assets uses: actions/upload-artifact@v4.3.6 diff --git a/.github/workflows/push-container-images.yml b/.github/workflows/push-container-images.yml index d615a59ef..511ae1c9c 100644 --- a/.github/workflows/push-container-images.yml +++ b/.github/workflows/push-container-images.yml @@ -52,4 +52,4 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: particular/servicepulse - readme-filepath: ./src/Container/README.md \ No newline at end of file + readme-filepath: ./Container-README.md \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 917794c92..e83d8daaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,31 +121,26 @@ jobs: - name: Install MinVer CLI run: dotnet tool install --global minver-cli - name: Determine version - run: echo "MinVerVersion=$(minver)" >> $GITHUB_ENV + shell: pwsh + run: | + # Read settings from Custom.Build.props + [xml]$xml = Get-Content ./src/Custom.Build.props + $minMajorMinor = $xml.selectNodes('/Project/PropertyGroup/MinVerMinimumMajorMinor').InnerText + $autoIncrement = $xml.selectNodes('/Project/PropertyGroup/MinVerAutoIncrement').InnerText + echo "MinVerMinimumMajorMinor=$minMajorMinor, MinVerAutoIncrement=$autoIncrement" + if (-not ($minMajorMinor -and $autoIncrement)) { + throw "Missing MinVer settings in Custom.Build.props" + } + + # Execute MinVer + echo "MinVerVersion=$(minver --minimum-major-minor $minMajorMinor --auto-increment $autoIncrement)" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - name: Validate build version if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} uses: ./.github/actions/validate-version with: version: ${{ env.MinVerVersion }} - - name: Set up Node.js - uses: actions/setup-node@v4.0.3 - with: - node-version: 21.6.x - - name: Build Frontend - run: .\build.ps1 - working-directory: src/ServicePulse.Host - shell: pwsh - - name: Update app.constants.js with MinVerVersion - run: | - $filename = "src/ServicePulse.Host/app/js/app.constants.js" - (Get-Content $filename).replace("1.2.0", "${{ env.MinVerVersion }}") | Set-Content $filename - shell: pwsh - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.6.1 - - name: Pull nginx:stable-alpine and determine digest - run: | - docker pull nginx:stable-alpine - echo "NGINX_DIGEST=$(docker inspect -f json nginx:stable-alpine | jq -r .[0].RepoDigests[0] | cut -d@ -f2)" >> $GITHUB_ENV - name: Log in to GitHub container registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Build & inspect image @@ -153,11 +148,8 @@ jobs: TAG_NAME: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || env.MinVerVersion }} run: | docker buildx build --push --tag ghcr.io/particular/servicepulse:${{ env.TAG_NAME }} \ - --file src/Container/Dockerfile \ - --build-arg NGINX_TAGORDIGEST="@${{ env.NGINX_DIGEST }}" \ + --file src/ServicePulse/Dockerfile \ --build-arg VERSION=${{ env.MinVerVersion }} \ - --build-arg GITHUB_SHA=${{ github.sha }} \ - --build-arg GITHUB_REF_NAME=${{ github.ref.name }} \ --annotation "index:org.opencontainers.image.title=ServicePulse" \ --annotation "index:org.opencontainers.image.description=ServicePulse provides real-time production monitoring for distributed applications. It monitors the health of a system's endpoints, detects processing errors, sends failed messages for reprocessing, and ensures the specific environment's needs are met, all in one consolidated dashboard." \ --annotation "index:org.opencontainers.image.created=$(date '+%FT%TZ')" \ @@ -168,6 +160,6 @@ jobs: --annotation "index:org.opencontainers.image.source=https://github.com/${{ github.repository }}/tree/${{ github.sha }}" \ --annotation "index:org.opencontainers.image.url=https://hub.docker.com/r/particular/servicepulse" \ --annotation "index:org.opencontainers.image.documentation=https://docs.particular.net/servicepulse/" \ - --annotation "index:org.opencontainers.image.base.name=nginx@${{ env.NGINX_DIGEST }}" \ - --platform linux/arm64,linux/arm,linux/amd64 . - docker buildx imagetools inspect ghcr.io/particular/servicepulse:${{ env.TAG_NAME }} \ No newline at end of file + --annotation "index:org.opencontainers.image.base.name=mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled-composite" \ + --platform linux/arm64,linux/amd64 . + docker buildx imagetools inspect ghcr.io/particular/servicepulse:${{ env.TAG_NAME }} diff --git a/src/Container/README.md b/Container-README.md similarity index 82% rename from src/Container/README.md rename to Container-README.md index cf99a75ae..7b531f7d9 100644 --- a/src/Container/README.md +++ b/Container-README.md @@ -7,20 +7,20 @@ This document describes basic usage and information related to the ServicePulse The following is the most basic way to create a ServicePulse container using [Docker](https://www.docker.com/): ```shell -docker run -p 9090:90 particular/servicepulse:latest +docker run -p 9090:9090 particular/servicepulse:latest ``` ### Environment Variables -- **`SERVICECONTROL_URL`**: _Default_: `http://localhost:33333/api/`. The url to your ServiceControl instance -- **`MONITORING_URLS`**: _Default_: `['http://localhost:33633/']`. A JSON array of URLs to your monitoring instances +- **`SERVICECONTROL_URL`**: _Default_: `http://localhost:33333`. The url to your ServiceControl instance +- **`MONITORING_URL`**: _Default_: `http://localhost:33633`. The url to your monitoring instance - **`DEFAULT_ROUTE`**: _Default_: `/dashboard`. The default page that should be displayed when visiting the site - **`SHOW_PENDING_RETRY`** _Default_: `false`. Set to `true` to show details of pending retries -It may be desireable to run the ServiceControl services in an isolated network. When doing so ServicePulse must be configured to connect to those services using environment variables: +It may be desireable to run the ServiceControl services in an isolated network. When doing so, ServicePulse must be configured to connect to those services using environment variables: ```shell -docker run -p 9090:90 -e SERVICECONTROL_URL="http://servicecontrol:33333/api/" -e MONITORING_URLS="['http://servicecontrol-monitoring:33633']" particular/servicepulse:latest +docker run -p 9090:9090 -e SERVICECONTROL_URL="http://servicecontrol:33333" -e MONITORING_URL="http://servicecontrol-monitoring:33633" particular/servicepulse:latest ``` Or as part of a [Docker Compose services specification](https://docs.docker.com/compose/compose-file/05-services/): @@ -29,10 +29,10 @@ Or as part of a [Docker Compose services specification](https://docs.docker.com/ services: servicepulse: ports: - - 9090:90 + - 9090:9090 environment: - - SERVICECONTROL_URL=http://servicecontrol:33333/api/ - - MONITORING_URLS=['http://servicecontrol-monitoring:33633'] + - SERVICECONTROL_URL=http://servicecontrol:33333 + - MONITORING_URL=http://servicecontrol-monitoring:33633 image: particular/servicepulse:latest ``` @@ -68,9 +68,9 @@ The major version tag is never added to images pushed to [the GitHub Container R The latest release within a minor version will be tagged with `{major}.{minor}` on images pushed to Docker Hub. This allows users to target the latest patch within a specific minor version. -## Built With +## Image architecture -This image is built from the stable Alpine version of the [nginx official Docker image](https://hub.docker.com/_/nginx/). +This image is a multi-arch image based on the `mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled-composite` base image supporting `linux/arm64` and `linux/amd64`. ## Contributing diff --git a/README.md b/README.md index b200b42dd..719d6b242 100644 --- a/README.md +++ b/README.md @@ -137,4 +137,4 @@ ServicePulse is supported on the following desktop browser versions: ## Container image development -A Dockerfile for ServicePulse resides within the [`src/Container`](https://github.com/Particular/ServicePulse/tree/master/src/Container) folder. The container images are all built as part of the [release workflow](https://github.com/Particular/ServicePulse/blob/master/.github/workflows/release.yml) and staged in the [Github Container Registry](https://github.com/Particular/ServicePulse/pkgs/container/servicepulse). For branches with PRs the image will be tagged with the pr number, e.g. `pr-1234`. +A Dockerfile for ServicePulse resides within the [`src/ServicePulse`](https://github.com/Particular/ServicePulse/tree/master/src/ServicePulse) folder. The container images are all built as part of the [release workflow](https://github.com/Particular/ServicePulse/blob/master/.github/workflows/release.yml) and staged in the [Github Container Registry](https://github.com/Particular/ServicePulse/pkgs/container/servicepulse). For branches with PRs, the image will be tagged with the PR number, e.g. `pr-1234`. diff --git a/src/.gitignore b/src/.gitignore index 982a82423..281b3f9a9 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -8,11 +8,8 @@ src/scaffolding.config ServicePulse.Host.Tests/node_modules package-lock.json -# bower artifacts -ServicePulse.Host/app/bower_components - # Chutzpah folders _Chutzpah.*/ -# Webpack results -ServicePulse.Host/angular/app/modules/dist +# ServicePulse.Host app +ServicePulse.Host/app diff --git a/src/Container/Dockerfile b/src/Container/Dockerfile deleted file mode 100644 index 3099e867d..000000000 --- a/src/Container/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -ARG NGINX_TAGORDIGEST=":1.26.1-alpine" -FROM nginx$NGINX_TAGORDIGEST - -ARG VERSION -ARG GITHUB_SHA -ARG GITHUB_REF_NAME - -LABEL org.opencontainers.image.title="ServicePulse" \ - org.opencontainers.image.description="ServicePulse provides real-time production monitoring for distributed applications. It monitors the health of a system's endpoints, detects processing errors, sends failed messages for reprocessing, and ensures the specific environment's needs are met, all in one consolidated dashboard." \ - org.opencontainers.image.authors="Particular Software" \ - org.opencontainers.image.vendor="Particular Software" \ - org.opencontainers.image.source="https://github.com/particular/servicepulse" \ - org.opencontainers.image.documentation="https://github.com/Particular/ServicePulse/blob/master/src/Container/README.md" \ - org.opencontainers.image.licenses="Commercial OR RPL-1.5" \ - org.opencontainers.image.version=$VERSION \ - org.opencontainers.image.revision=$GITHUB_SHA \ - org.opencontainers.image.base.digest=$NGINX_TAGORDIGEST \ - org.opencontainers.image.base.name="nginx:stable-alpine" \ - com.particular.github.ref.name=$GITHUB_REF_NAME \ - com.particular.support.url="https://particular.net/support" \ - maintainer="Particular Software" - -ENV SERVICECONTROL_URL="http://localhost:33333/api/" -ENV MONITORING_URLS="['http://localhost:33633/']" -ENV DEFAULT_ROUTE="/dashboard" -ENV SHOW_PENDING_RETRY="false" - -COPY /src/ServicePulse.Host/app /usr/share/nginx/html -ADD /src/Container/app.constants.template /usr/share/nginx/html/js/app.constants.template - -RUN sed -i -e "s,__VERSION__,$VERSION,g" /usr/share/nginx/html/js/app.constants.template - -ADD /src/Container/nginx.conf /etc/nginx/ -ADD --chown=root:root --chmod=755 /src/Container/updateconstants.sh /docker-entrypoint.d/40-update-servicepulse-constants.sh - -EXPOSE 90 \ No newline at end of file diff --git a/src/Container/app.constants.template b/src/Container/app.constants.template deleted file mode 100644 index de718e99d..000000000 --- a/src/Container/app.constants.template +++ /dev/null @@ -1,7 +0,0 @@ -window.defaultConfig = { - default_route: '$DEFAULT_ROUTE', - version: '__VERSION__', - service_control_url: '$SERVICECONTROL_URL', - monitoring_urls: $MONITORING_URLS, - showPendingRetry: $SHOW_PENDING_RETRY, -}; diff --git a/src/Container/nginx.conf b/src/Container/nginx.conf deleted file mode 100644 index 32744d975..000000000 --- a/src/Container/nginx.conf +++ /dev/null @@ -1,18 +0,0 @@ -events { - worker_connections 1024; -} - -http { - include mime.types; - sendfile on; - server { - root /usr/share/nginx/html/; - index index.html; - server_name localhost; - listen 90; - - location / { - try_files $uri $uri/ /index.html; - } - } -} \ No newline at end of file diff --git a/src/Container/updateconstants.sh b/src/Container/updateconstants.sh deleted file mode 100644 index c2cdfd507..000000000 --- a/src/Container/updateconstants.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -if [ ! -f /usr/share/nginx/html/js/app.constants.js ] -then - envsubst < /usr/share/nginx/html/js/app.constants.template > /usr/share/nginx/html/js/app.constants.js -fi \ No newline at end of file diff --git a/src/Custom.Build.props b/src/Custom.Build.props index a649a45d4..91513a563 100644 --- a/src/Custom.Build.props +++ b/src/Custom.Build.props @@ -8,5 +8,5 @@ all - + diff --git a/src/Frontend/vite.config.ts b/src/Frontend/vite.config.ts index 8e8eb52ed..b5c4adfed 100644 --- a/src/Frontend/vite.config.ts +++ b/src/Frontend/vite.config.ts @@ -60,7 +60,6 @@ export default defineConfig({ }, base: "./", build: { - outDir: "../ServicePulse.Host/app", emptyOutDir: true, sourcemap: true, rollupOptions: { diff --git a/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj b/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj index a1954acfd..3752a91b5 100644 --- a/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj +++ b/src/Particular.PlatformSample.ServicePulse/Particular.PlatformSample.ServicePulse.csproj @@ -1,28 +1,32 @@ - net48 + net8.0 false true Particular ServicePulse binaries for use by Particular.PlatformSample. Not intended for use outside of Particular.PlatformSample. https://docs.particular.net/servicepulse/ + $(TargetsForTfmSpecificContentInPackage);AddFilesToPackage + $(NoWarn);NU5100 - + - - - - + + + + + + diff --git a/src/Particular.PlatformSample.ServicePulse/buildProps/build/Particular.PlatformSample.ServicePulse.props b/src/Particular.PlatformSample.ServicePulse/buildProps/build/Particular.PlatformSample.ServicePulse.props index 7996f17d5..d8c9cbab2 100644 --- a/src/Particular.PlatformSample.ServicePulse/buildProps/build/Particular.PlatformSample.ServicePulse.props +++ b/src/Particular.PlatformSample.ServicePulse/buildProps/build/Particular.PlatformSample.ServicePulse.props @@ -1,11 +1,7 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - + - + diff --git a/src/Particular.PlatformSample.ServicePulse/buildProps/buildMultiTargeting/Particular.PlatformSample.ServicePulse.props b/src/Particular.PlatformSample.ServicePulse/buildProps/buildMultiTargeting/Particular.PlatformSample.ServicePulse.props index 68b90608e..3481c938a 100644 --- a/src/Particular.PlatformSample.ServicePulse/buildProps/buildMultiTargeting/Particular.PlatformSample.ServicePulse.props +++ b/src/Particular.PlatformSample.ServicePulse/buildProps/buildMultiTargeting/Particular.PlatformSample.ServicePulse.props @@ -1,8 +1,4 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - + diff --git a/src/ServicePulse.Host.Tests/Api/APIApprovals.cs b/src/ServicePulse.Host.Tests/Api/APIApprovals.cs deleted file mode 100644 index 7a49df2c8..000000000 --- a/src/ServicePulse.Host.Tests/Api/APIApprovals.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NUnit.Framework; -using Particular.Approvals; -using System.IO; - -class APIApprovals -{ - [Test] - public void PlatformSampleApprovals() - { - //HINT: If this test fails the Particular.PlatformSample project's app.constants.js probably needs to be updated - Approver.Verify(File.ReadAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "app.constants.js"))); - } -} \ No newline at end of file diff --git a/src/ServicePulse.Host.Tests/ApprovalFiles/APIApprovals.PlatformSampleApprovals.approved.txt b/src/ServicePulse.Host.Tests/ApprovalFiles/APIApprovals.PlatformSampleApprovals.approved.txt deleted file mode 100644 index d138f9910..000000000 --- a/src/ServicePulse.Host.Tests/ApprovalFiles/APIApprovals.PlatformSampleApprovals.approved.txt +++ /dev/null @@ -1,7 +0,0 @@ -window.defaultConfig = { - default_route: '/dashboard', - version: '1.2.0', - service_control_url: 'http://localhost:33333/api/', - monitoring_urls: ['http://localhost:33633/'], - showPendingRetry: false, -}; diff --git a/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs b/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs index d85e6554d..f6036cf31 100644 --- a/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs +++ b/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs @@ -129,7 +129,7 @@ public async Task Should_find_prefer_constants_file_on_disk_over_embedded_if_bot } }; await middleware.Invoke(context); - const long sizeOfFileOnDisk = 232; // this is the /app/js/app.constants.js file + const long sizeOfFileOnDisk = 215; // this is the /app/js/app.constants.js file Assert.That(context.Response.ContentLength, Is.EqualTo(sizeOfFileOnDisk)); Assert.That(context.Response.ContentType, Is.EqualTo("application/javascript")); } diff --git a/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj b/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj index 9d3d8f2dc..741e71f1c 100644 --- a/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj +++ b/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj @@ -1,4 +1,4 @@ - + net48 @@ -9,10 +9,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -21,17 +18,8 @@ - - - - - - PreserveNewest - - - %(RelativeDir)%(Filename)%(Extension) - PreserveNewest - + + diff --git a/src/ServicePulse.Host.Tests/VerifyAppConstantsJSTextReplacement.cs b/src/ServicePulse.Host.Tests/VerifyAppConstantsJSTextReplacement.cs index 9013943d6..d9263acee 100644 --- a/src/ServicePulse.Host.Tests/VerifyAppConstantsJSTextReplacement.cs +++ b/src/ServicePulse.Host.Tests/VerifyAppConstantsJSTextReplacement.cs @@ -17,7 +17,7 @@ public class VerifyAppConstantsJSTextReplacement [Test] public void App_constants_js_validation() { - var pathToConfig = Path.Combine(TestContext.CurrentContext.TestDirectory, "app.constants.js"); + var pathToConfig = Path.Combine(TestContext.CurrentContext.TestDirectory, "app", "js", "app.constants.js"); Assert.That(File.Exists(pathToConfig), Is.True, "app.constants.js does not exist - this will break installation code"); var config = File.ReadAllText(pathToConfig); diff --git a/src/ServicePulse.Host.Tests/app/js/app.constants.js b/src/ServicePulse.Host.Tests/app/js/app.constants.js deleted file mode 100644 index a2e1af98a..000000000 --- a/src/ServicePulse.Host.Tests/app/js/app.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -window.defaultConfig = { - default_route: '/dashboard', - version: '1.2.0', - service_control_url: 'http://some.host.com:33333/api/', - monitoring_urls: ['http://some.host.com:33633/'], - showPendingRetry: true, -}; diff --git a/src/ServicePulse.Host/ServicePulse.Host.csproj b/src/ServicePulse.Host/ServicePulse.Host.csproj index f01671369..98160bca7 100644 --- a/src/ServicePulse.Host/ServicePulse.Host.csproj +++ b/src/ServicePulse.Host/ServicePulse.Host.csproj @@ -1,4 +1,4 @@ - + net48 @@ -25,8 +25,7 @@ - - PreserveNewest - + + \ No newline at end of file diff --git a/src/ServicePulse.Host/app/js/app.constants.js b/src/ServicePulse.Host/app/js/app.constants.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/ServicePulse.Host/build.ps1 b/src/ServicePulse.Host/build.ps1 index f77e567cc..517cb0a2a 100644 --- a/src/ServicePulse.Host/build.ps1 +++ b/src/ServicePulse.Host/build.ps1 @@ -1,17 +1,21 @@ -$AppOutputFolder = "app" -$FrontendSourceFolder = "../Frontend" +$ScriptPath = $PSCommandPath | Split-Path +$AppOutputFolder = $ScriptPath + "/app" +$FrontendSourceFolder = $ScriptPath + "/../Frontend" -if (Test-Path $AppOutputFolder) -{ - Get-ChildItem -Path $AppOutputFolder -Include *.* -File -Recurse | foreach { $_.Delete()} +if (Test-Path $AppOutputFolder) { + Remove-Item $AppOutputFolder -Force -Recurse } New-Item -ItemType Directory -Force -Path $AppOutputFolder -cd $FrontendSourceFolder +Push-Location $FrontendSourceFolder npm install -Remove-Item -Path "./public/mockServiceWorker.js" npm run build +Remove-Item -Path "./dist/mockServiceWorker.js" +Pop-Location + +Copy-Item -path $FrontendSourceFolder/dist/* -Destination $AppOutputFolder -Recurse + if ( $? -eq $false ) { exit $LastExitCode } \ No newline at end of file diff --git a/src/ServicePulse.sln b/src/ServicePulse.sln index f46763e6a..2d35b6f08 100644 --- a/src/ServicePulse.sln +++ b/src/ServicePulse.sln @@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Custom.Build.props = Custom.Build.props EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePulse", "ServicePulse\ServicePulse.csproj", "{084808CF-4B93-4097-BFA1-2604AA7B4594}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +84,10 @@ Global {67FD72C8-A459-4235-AD10-3DF5B68A3172}.Debug|Any CPU.Build.0 = Debug|Any CPU {67FD72C8-A459-4235-AD10-3DF5B68A3172}.Release|Any CPU.ActiveCfg = Release|Any CPU {67FD72C8-A459-4235-AD10-3DF5B68A3172}.Release|Any CPU.Build.0 = Release|Any CPU + {084808CF-4B93-4097-BFA1-2604AA7B4594}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {084808CF-4B93-4097-BFA1-2604AA7B4594}.Debug|Any CPU.Build.0 = Debug|Any CPU + {084808CF-4B93-4097-BFA1-2604AA7B4594}.Release|Any CPU.ActiveCfg = Release|Any CPU + {084808CF-4B93-4097-BFA1-2604AA7B4594}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ServicePulse/ConstantsFile.cs b/src/ServicePulse/ConstantsFile.cs new file mode 100644 index 000000000..f5708aa81 --- /dev/null +++ b/src/ServicePulse/ConstantsFile.cs @@ -0,0 +1,42 @@ +namespace ServicePulse; + +using System.Reflection; + +class ConstantsFile +{ + public static string GetContent() + { + var defaultRoute = Environment.GetEnvironmentVariable("DEFAULT_ROUTE") ?? "/dashboard"; + var version = GetVersionInformation(); + var showPendingRetry = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY") ?? "false"; + + var constantsFile = $$""" +window.defaultConfig = { + default_route: '{{defaultRoute}}', + version: '{{version}}', + service_control_url: '/api/', + monitoring_urls: ['/monitoring-api/'], + showPendingRetry: {{showPendingRetry}}, +} +"""; + + return constantsFile; + } + + static string GetVersionInformation() + { + var majorMinorPatch = "0.0.0"; + + var attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(); + + foreach (var attribute in attributes) + { + if (attribute.Key == "MajorMinorPatch") + { + majorMinorPatch = attribute.Value ?? "0.0.0"; + } + } + + return majorMinorPatch; + } +} diff --git a/src/ServicePulse/Dockerfile b/src/ServicePulse/Dockerfile new file mode 100644 index 000000000..4679a29ce --- /dev/null +++ b/src/ServicePulse/Dockerfile @@ -0,0 +1,34 @@ +# Frontend build image +FROM --platform=$BUILDPLATFORM node:latest AS frontend +WORKDIR / +COPY . . +WORKDIR /src/Frontend +RUN npm install +RUN npm run build + +# Host build image +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG TARGETARCH +WORKDIR / +ENV CI=true +COPY --from=frontend . . +RUN dotnet publish src/ServicePulse/ServicePulse.csproj -a $TARGETARCH -o /app + +# Host runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled-composite +ARG VERSION +WORKDIR /app + +LABEL org.opencontainers.image.source="https://github.com/particular/servicepulse" \ + org.opencontainers.image.authors="Particular Software" \ + org.opencontainers.image.url=https://docs.particular.net/servicepulse/ \ + org.opencontainers.image.documentation="https://docs.particular.net/servicepulse/" \ + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.title="ServicePulse" \ + org.opencontainers.image.description="ServicePulse provides real-time production monitoring for distributed applications. It monitors the health of a system's endpoints, detects processing errors, sends failed messages for reprocessing, and ensures the specific environment's needs are met, all in one consolidated dashboard." + +ENV ASPNETCORE_HTTP_PORTS=9090 +EXPOSE 9090 +COPY --from=build /app . +USER $APP_UID +ENTRYPOINT ["./ServicePulse"] \ No newline at end of file diff --git a/src/ServicePulse/Program.cs b/src/ServicePulse/Program.cs new file mode 100644 index 000000000..c085e3f08 --- /dev/null +++ b/src/ServicePulse/Program.cs @@ -0,0 +1,31 @@ +using System.Net.Mime; +using Microsoft.Extensions.FileProviders; +using ServicePulse; + +var builder = WebApplication.CreateBuilder(args); + +var (routes, clusters) = ReverseProxy.GetConfiguration(); +builder.Services.AddReverseProxy().LoadFromMemory(routes, clusters); + +var app = builder.Build(); + +var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "wwwroot"); +var fileProvider = new CompositeFileProvider(builder.Environment.WebRootFileProvider, manifestEmbeddedFileProvider); + +var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; +app.UseDefaultFiles(defaultFilesOptions); + +var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; +app.UseStaticFiles(staticFileOptions); + +app.MapReverseProxy(); + +var constantsFile = ConstantsFile.GetContent(); + +app.MapGet("/js/app.constants.js", (HttpContext context) => +{ + context.Response.ContentType = MediaTypeNames.Text.JavaScript; + return constantsFile; +}); + +app.Run(); diff --git a/src/ServicePulse/Properties/launchSettings.json b/src/ServicePulse/Properties/launchSettings.json new file mode 100644 index 000000000..8ea44c65f --- /dev/null +++ b/src/ServicePulse/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53791", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5291", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/ServicePulse/ReverseProxy.cs b/src/ServicePulse/ReverseProxy.cs new file mode 100644 index 000000000..0446a238d --- /dev/null +++ b/src/ServicePulse/ReverseProxy.cs @@ -0,0 +1,110 @@ +namespace ServicePulse; + +using System.Text.Json; +using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.Transforms; + +static class ReverseProxy +{ + public static (List routes, List clusters) GetConfiguration() + { + var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333"; + var serviceControlUri = new Uri(serviceControlUrl); + + var monitoringUrls = ParseLegacyMonitoringValue(Environment.GetEnvironmentVariable("MONITORING_URLS")); + var monitoringUrl = Environment.GetEnvironmentVariable("MONITORING_URL"); + + monitoringUrl ??= monitoringUrls; + monitoringUrl ??= "http://localhost:33633"; + + var monitoringUri = new Uri(monitoringUrl); + + var serviceControlInstance = new ClusterConfig + { + ClusterId = "serviceControlInstance", + Destinations = new Dictionary + { + { "instance", new DestinationConfig { Address = serviceControlUri.GetLeftPart(UriPartial.Authority) } } + } + }; + + var monitoringInstance = new ClusterConfig + { + ClusterId = "monitoringInstance", + Destinations = new Dictionary + { + { "instance", new DestinationConfig { Address = monitoringUri.GetLeftPart(UriPartial.Authority) } } + } + }; + + var serviceControlRoute = new RouteConfig() + { + RouteId = "serviceControlRoute", + ClusterId = nameof(serviceControlInstance), + Match = new RouteMatch + { + Path = "/api/{**catch-all}" + } + }; + + var monitoringRoute = new RouteConfig() + { + RouteId = "monitoringRoute", + ClusterId = nameof(monitoringInstance), + Match = new RouteMatch + { + Path = "/monitoring-api/{**catch-all}" + } + }.WithTransformPathRemovePrefix("/monitoring-api"); + + var routes = new List + { + serviceControlRoute, + monitoringRoute + }; + + var clusters = new List + { + serviceControlInstance, + monitoringInstance + }; + + return (routes, clusters); + } + + static string? ParseLegacyMonitoringValue(string? value) + { + if (value is null) + { + return null; + } + + var cleanedValue = value.Replace('\'', '"'); + var json = $$"""{"Addresses":{{cleanedValue}}}"""; + + MonitoringUrls? result; + + try + { + result = JsonSerializer.Deserialize(json); + } + catch (JsonException) + { + return null; + } + + var addresses = result?.Addresses; + + if (addresses is not null && addresses.Length > 0) + { + return addresses[0]; + } + + return null; + } + + class MonitoringUrls + { + public string[] Addresses { get; set; } = []; + } +} diff --git a/src/ServicePulse/ServicePulse.csproj b/src/ServicePulse/ServicePulse.csproj new file mode 100644 index 000000000..22c522109 --- /dev/null +++ b/src/ServicePulse/ServicePulse.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + false + + + + + + + + + + + + + diff --git a/src/ServicePulse/appsettings.Development.json b/src/ServicePulse/appsettings.Development.json new file mode 100644 index 000000000..23aa4edee --- /dev/null +++ b/src/ServicePulse/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Yarp.ReverseProxy": "Warning" + } + } +} diff --git a/src/ServicePulse/appsettings.json b/src/ServicePulse/appsettings.json new file mode 100644 index 000000000..ea161078a --- /dev/null +++ b/src/ServicePulse/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.DataProtection": "Error", + "Yarp.ReverseProxy": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/ServicePulse/build.ps1 b/src/ServicePulse/build.ps1 new file mode 100644 index 000000000..6817b0c91 --- /dev/null +++ b/src/ServicePulse/build.ps1 @@ -0,0 +1,11 @@ +$ScriptPath = $PSCommandPath | Split-Path +$FrontendSourceFolder = $ScriptPath + "/../Frontend" + +Push-Location $FrontendSourceFolder +npm install +npm run build +Pop-Location + +if ( $? -eq $false ) { + exit $LastExitCode +} \ No newline at end of file