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