Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7f8d1bb
Split application settings and host settings
mikeminutillo Jan 15, 2026
e55481d
Extract App from Host
mikeminutillo Jan 15, 2026
12235ac
Extract core from host
mikeminutillo Jan 15, 2026
ed453f2
Add public API documentation
mikeminutillo Jan 15, 2026
dca04e2
Rename Settings to make it clear in other contexts
mikeminutillo Jan 15, 2026
a137919
Skip AutomaticVersionRange on MS Dependency
mikeminutillo Jan 15, 2026
adea759
Expect an additional nuget
mikeminutillo Jan 15, 2026
411bd9c
Serving files off disk is optional
mikeminutillo Jan 16, 2026
1c846d4
Indicate to UI if running in embedded mode
mikeminutillo Jan 16, 2026
d9ae162
Adjust SP UI when embedded
mikeminutillo Jan 16, 2026
8e41237
Cleanup
mikeminutillo Jan 16, 2026
2337efa
Update StaticMiddlewareTests.cs
mikeminutillo Jan 23, 2026
0564938
Use passed ServiceControl url if embedded
mikeminutillo Jan 23, 2026
924fccc
Allow serving constants file anonymously
mikeminutillo Jan 28, 2026
689c3b1
Remove unused method
mikeminutillo Jan 28, 2026
e40dbd9
Remove unused method 2
mikeminutillo Jan 28, 2026
ec2027c
Apply suggestions from code review
mikeminutillo Feb 4, 2026
db6ba53
Cleanup
mikeminutillo Feb 4, 2026
381fb16
Fix whitespace
mikeminutillo Feb 4, 2026
d6d3607
Put hosting extension in namespace
mikeminutillo Feb 6, 2026
dea8d9a
Change embedded to integrated
mikeminutillo Feb 6, 2026
ebe4049
Update to .NET 10
mikeminutillo Feb 6, 2026
bdb4f34
Fix dotnet versions used for actions
mikeminutillo Feb 6, 2026
1ae0d00
Use global json file
mikeminutillo Feb 6, 2026
a04eb47
Rename ServicePulse.Core to Particular.ServicePulse.Core
mikeminutillo Feb 6, 2026
ffb6f3d
Address comments
mikeminutillo Feb 24, 2026
5936aeb
Push package to testing feed
mikeminutillo Feb 24, 2026
27da5ce
Update package description
mikeminutillo Feb 24, 2026
72c4de7
Update labels and annotations for .NET 10 docker images
mikeminutillo Feb 26, 2026
df6437d
Update embedded file provider
mikeminutillo Feb 26, 2026
af439d8
Fix flaky sorting tests
mikeminutillo Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 8.0.x
global-json-file: global.json
- name: Set up Node.js
uses: actions/setup-node@v6.2.0
with:
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 7.0.x
global-json-file: global.json
- name: Set up Node.js
uses: actions/setup-node@v6.2.0
with:
Expand Down Expand Up @@ -82,7 +82,7 @@ jobs:
$nugetsCount = (Get-ChildItem -Recurse -File nugets).Count

$expectedAssetsCount = 1
$expectedNugetsCount = 1
$expectedNugetsCount = 2

if ($assetsCount -ne $expectedAssetsCount)
{
Expand All @@ -95,6 +95,9 @@ jobs:
Write-Host Nugets: Expected $expectedNugetsCount but found $nugetsCount
exit -1
}
- name: Push packages to testing feed
if: ${{ github.event_name == 'workflow_dispatch' }}
run: dotnet nuget push nugets\*.nupkg --api-key ${{ secrets.FEEDZIO_PUBLISH_API_KEY }} --source "${{ vars.PARTICULAR_TESTING_FEED_URL }}"
# Deploy to Octopus
- name: Deploy
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
Expand Down Expand Up @@ -170,7 +173,7 @@ jobs:
org.opencontainers.image.created=${{ steps.date.outputs.date }}
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.base.name=mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled-composite
org.opencontainers.image.base.name=mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-composite
annotations: |
index:org.opencontainers.image.source=https://github.com/Particular/ServicePulse/tree/${{ github.sha }}
index:org.opencontainers.image.authors="Particular Software"
Expand All @@ -182,7 +185,7 @@ jobs:
index:org.opencontainers.image.created=${{ steps.date.outputs.date }}
index:org.opencontainers.image.title=ServicePulse
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.
index:org.opencontainers.image.base.name=mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled-composite
index:org.opencontainers.image.base.name=mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-composite
file: src/ServicePulse/Dockerfile
tags: ghcr.io/particular/servicepulse:${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || env.MinVerVersion }}

3 changes: 2 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"sdk": {
"version": "8.0.400",
"version": "10.0.0",
"allowPrerelease": false,
"rollForward": "latestFeature"
}
}
1 change: 1 addition & 0 deletions src/Frontend/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {
service_control_url: string;
monitoring_urls: string[];
showPendingRetry: boolean;
isIntegrated?: boolean;
};
}
}
1 change: 1 addition & 0 deletions src/Frontend/public/js/app.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ window.defaultConfig = {
service_control_url: 'http://localhost:33333/api/',
monitoring_urls: ['http://localhost:33633/'],
showPendingRetry: false,
isIntegrated: false,
};
14 changes: 9 additions & 5 deletions src/Frontend/src/components/PageFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const environment = environmentAndVersionsStore.environment;
const licenseStore = useLicenseStore();
const { licenseStatus, license } = licenseStore;
const isMonitoringEnabled = monitoringClient.isMonitoringEnabled;
const isIntegrated = window.defaultConfig.isIntegrated;

const scAddressTooltip = computed(() => {
return `ServiceControl URL ${serviceControlClient.url}`;
Expand All @@ -44,11 +45,14 @@ const { configuration } = storeToRefs(configurationStore);
<RouterLink :to="routeLinks.configuration.endpointConnection.link">Connect new endpoint</RouterLink>
</span>

<span v-if="!newVersions.newSPVersion.newspversion && environment.sp_version"> ServicePulse v{{ environment.sp_version }} </span>
<span v-if="newVersions.newSPVersion.newspversion && environment.sp_version">
ServicePulse v{{ environment.sp_version }} (<FAIcon v-if="newVersions.newSPVersion.newspversionnumber" class="footer-icon fake-link" :icon="faArrowTurnUp" />
<a :href="newVersions.newSPVersion.newspversionlink" target="_blank">v{{ newVersions.newSPVersion.newspversionnumber }} available</a>)
</span>
<span v-if="isIntegrated"> Integrated ServicePulse </span>
<template v-else-if="environment.sp_version">
<span v-if="!newVersions.newSPVersion.newspversion"> ServicePulse v{{ environment.sp_version }} </span>
<span v-else>
ServicePulse v{{ environment.sp_version }} (<FAIcon v-if="newVersions.newSPVersion.newspversionnumber" class="footer-icon fake-link" :icon="faArrowTurnUp" />
<a :href="newVersions.newSPVersion.newspversionlink" target="_blank">v{{ newVersions.newSPVersion.newspversionnumber }} available</a>)
</span>
</template>
<span :title="scAddressTooltip">
Service Control:
<span class="connected-status" v-if="connectionState.connected && !connectionState.connecting">
Expand Down
21 changes: 14 additions & 7 deletions src/Frontend/src/components/configuration/PlatformConnections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const serviceControlValid = ref<boolean | null>(null);
const testingMonitoring = ref(false);
const monitoringValid = ref<boolean | null>(null);
const connectionSaved = ref<boolean | null>(null);
const isIntegrated = window.defaultConfig.isIntegrated;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a thought that all the app constants should be loaded in a dedicated module, so that the direct window reference is constrained to a single location and can be changed out more easily


async function testServiceControlUrl() {
if (localServiceControlUrl.value) {
Expand Down Expand Up @@ -64,10 +65,15 @@ function saveConnections() {
}

function updateServiceControlUrls() {
if (!localServiceControlUrl.value) {
throw new Error("ServiceControl URL is mandatory");
} else if (!localServiceControlUrl.value.endsWith("/")) {
localServiceControlUrl.value += "/";
const params = new URLSearchParams();

if (!isIntegrated) {
if (!localServiceControlUrl.value) {
throw new Error("ServiceControl URL is mandatory");
} else if (!localServiceControlUrl.value.endsWith("/")) {
localServiceControlUrl.value += "/";
}
params.set("scu", localServiceControlUrl.value);
}

if (!localMonitoringUrl.value) {
Expand All @@ -76,8 +82,6 @@ function updateServiceControlUrls() {
localMonitoringUrl.value += "/";
}

const params = new URLSearchParams();
params.set("scu", localServiceControlUrl.value);
params.set("mu", localMonitoringUrl.value);
window.location.search = `?${params.toString()}`;
}
Expand All @@ -94,11 +98,14 @@ function updateServiceControlUrls() {
<div class="col-7 form-group">
<label for="serviceControlUrl">
CONNECTION URL
<template v-if="isIntegrated">
<span>(INTEGRATED)</span>
</template>
<template v-if="connectionState.unableToConnect">
<span class="failed-validation"><FAIcon :icon="faExclamationTriangle" /> Unable to connect </span>
</template>
</label>
<input type="text" id="serviceControlUrl" name="serviceControlUrl" v-model="localServiceControlUrl" class="form-control" style="color: #000" required />
<input type="text" id="serviceControlUrl" name="serviceControlUrl" v-model="localServiceControlUrl" class="form-control" style="color: #000" required :disabled="isIntegrated" />
</div>

<div class="col-5 no-side-padding">
Expand Down
4 changes: 4 additions & 0 deletions src/Frontend/src/components/serviceControlClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class ServiceControlClient {
}

private getUrl() {
if (window.defaultConfig?.isIntegrated && window.defaultConfig.service_control_url?.length) {
return window.defaultConfig.service_control_url;
}

const searchParams = new URLSearchParams(window.location.search);
const scu = searchParams.get("scu");
const existingScu = window.localStorage.getItem("scu");
Expand Down
21 changes: 13 additions & 8 deletions src/Frontend/test/specs/monitoring/sorting-endpoints.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from "vitest";
import { test, describe } from "../../drivers/vitest/driver";
import { waitFor } from "@testing-library/vue";
import { groupEndpointsBy } from "./actions/groupEndpointsBy";
import { endpointGroupNames } from "./questions/endpointGroupNames";
import { endpointGroup } from "./questions/endpointGroup";
Expand Down Expand Up @@ -68,10 +69,12 @@ describe("FEATURE: Endpoint sorting", () => {
await sortEndpointsBy({ column: columnName.ENDPOINTNAME });

//Assert
expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Venus", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Earth"]);
expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint4", "Endpoint3"]);
expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint2", "Endpoint1"]);
expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint6", "Endpoint5"]);
await waitFor(() => {
expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Venus", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Earth"]);
expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint4", "Endpoint3"]);
expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint2", "Endpoint1"]);
expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint6", "Endpoint5"]);
});
});

test("EXAMPLE: Endpoints inside of the groups and group names should be sorted in ascending order when clicking twice on the endpoint name column title", async ({ driver }) => {
Expand All @@ -95,10 +98,12 @@ describe("FEATURE: Endpoint sorting", () => {
await sortEndpointsBy({ column: columnName.ENDPOINTNAME }); //Click the column title again for ascending

//Assert
expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Earth", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Venus"]);
expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint5", "Endpoint6"]);
expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint1", "Endpoint2"]);
expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint3", "Endpoint4"]);
await waitFor(() => {
expect(endpointGroupNames()).toEqual(["Universe.Solarsystem.Earth", "Universe.Solarsystem.Mercury", "Universe.Solarsystem.Venus"]);
expect(endpointGroup("Universe.Solarsystem.Earth").Endpoints).toEqual(["Endpoint5", "Endpoint6"]);
expect(endpointGroup("Universe.Solarsystem.Mercury").Endpoints).toEqual(["Endpoint1", "Endpoint2"]);
expect(endpointGroup("Universe.Solarsystem.Venus").Endpoints).toEqual(["Endpoint3", "Endpoint4"]);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<Description>Particular ServicePulse binaries for use by Particular.PlatformSample. Not intended for use outside of Particular.PlatformSample.</Description>
Expand Down
30 changes: 30 additions & 0 deletions src/Particular.ServicePulse.Core/ConstantsFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace ServicePulse;

using System.Reflection;

class ConstantsFile
{
public static string GetContent(ServicePulseSettings settings)
{
var version = GetVersionInformation();

var constantsFile = $$"""
window.defaultConfig = {
default_route: '{{settings.DefaultRoute}}',
version: '{{version}}',
service_control_url: '{{settings.ServiceControlUrl}}',
monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'],
showPendingRetry: {{settings.ShowPendingRetry.ToString().ToLower()}},
isIntegrated: {{settings.IsIntegrated.ToString().ToLower()}}
}
""";

return constantsFile;
}

static string GetVersionInformation()
=> typeof(ConstantsFile).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.SingleOrDefault(attribute => attribute.Key == "MajorMinorPatch")
?.Value ?? "0.0.0";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Description>Particular ServicePulse binaries for use by integrated ServicePulse. Not intended for use outside of ServiceControl.</Description>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.3" AutomaticVersionRange="false" />
<PackageReference Include="Particular.Packaging" Version="4.5.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="..\Frontend\dist\**\*" Exclude="..\Frontend\dist\js\**\*" LinkBase="wwwroot" />
</ItemGroup>

</Project>
37 changes: 37 additions & 0 deletions src/Particular.ServicePulse.Core/ServicePulseHostingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace ServicePulse;

using System.Net.Mime;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;

/// <summary>
/// Extensions for hosting ServicePulse within a WebApplication.
/// </summary>
public static class ServicePulseHostingExtensions
{
/// <summary>
/// Adds ServicePulse static file serving and configuration endpoint to the WebApplication.
/// </summary>
public static void UseServicePulse(this WebApplication app, ServicePulseSettings settings, IFileProvider? overrideFileProvider = null)
{
var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot");
IFileProvider fileProvider = overrideFileProvider is null
? manifestEmbeddedFileProvider
: new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider);

var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider };
app.UseDefaultFiles(defaultFilesOptions);

var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider };
app.UseStaticFiles(staticFileOptions);

var constantsFile = ConstantsFile.GetContent(settings);

app.MapGet("/js/app.constants.js", (HttpContext context) =>
{
context.Response.ContentType = MediaTypeNames.Text.JavaScript;
return constantsFile;
}).AllowAnonymous();
}
}
Loading