RunSignal.Core 0.2.0

dotnet add package RunSignal.Core --version 0.2.0
                    
NuGet\Install-Package RunSignal.Core -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RunSignal.Core" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RunSignal.Core" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="RunSignal.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RunSignal.Core --version 0.2.0
                    
#r "nuget: RunSignal.Core, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RunSignal.Core@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RunSignal.Core&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=RunSignal.Core&version=0.2.0
                    
Install as a Cake Tool

RunSignal for .NET

NuGet

Hangfire, Quartz.NET and BackgroundService monitoring, on OpenTelemetry. Get an alert when a recurring job is missed, fails, overruns or processes nothing, or when a worker loop stalls, with no code in your jobs.

Alerts for .NET background work that no HTTP status code describes: Hangfire and Quartz.NET jobs, BackgroundService loops and consumers. RunSignal records runs, items and heartbeats as OpenTelemetry metrics, with the same names as every other RunSignal SDK. The runsignal CLI turns them into New Relic alerts and dashboards, or Prometheus rules and a Grafana dashboard. The main README covers the concepts, the alerts and the backends.

Installation

dotnet add package RunSignal.DotNet           # registry, dependency injection, manifest
dotnet add package RunSignal.OpenTelemetry    # AddRunSignalInstrumentation()
dotnet add package RunSignal.Hangfire         # optional: Hangfire 1.8+
dotnet add package RunSignal.Quartz           # optional: Quartz.NET 3 (RunSignal.Quartz4 for Quartz.NET 4)
dotnet tool install -g RunSignal.Cli          # the runsignal CLI, for CI

The packages target net8.0; RunSignal.Quartz4 targets net10.0, like Quartz.NET 4.

Package Purpose
RunSignal.Core Workloads, runs, items, heartbeats, manifest. No dependencies beyond the BCL.
RunSignal.DotNet AddRunSignal(), AddWorkload(), keyed injection, startup discovery, manifest writing.
RunSignal.OpenTelemetry AddRunSignalInstrumentation() for metrics and traces, with duration buckets for jobs.
RunSignal.Hangfire Monitors every Hangfire 1.8+ job.
RunSignal.Quartz / RunSignal.Quartz4 Monitors every Quartz.NET 3 / Quartz.NET 4 job.

Setup

builder.Services.AddRunSignal()
    .AddWorkload("stock-sync", o => o.ExpectEvery = TimeSpan.FromMinutes(5))
    .AddWorkload("nightly-invoice", o =>
    {
        o.Schedule = "0 2 * * *";
        o.MaxDuration = TimeSpan.FromMinutes(30);
        o.MinProcessed = 100;
        o.Severity = Severity.Critical;
    });

builder.Services.AddOpenTelemetry()
    .WithMetrics(m => m.AddRunSignalInstrumentation().AddOtlpExporter())
    .WithTracing(t => t.AddRunSignalInstrumentation().AddOtlpExporter());

Inject a workload with [FromKeyedServices("nightly-invoice")] Workload workload. Without dependency injection, call WorkloadRegistry.Shared.Get("name") once and keep the result.

Recording work

A scheduled job, recorded as one run that succeeds, fails or is cancelled (when its token is cancelled):

public Task RunAsync(CancellationToken ct) => workload.RunAsync(async ct =>
{
    foreach (var order in await _orders.GetUnbilledAsync(ct))
    {
        await _invoices.CreateAsync(order, ct);
        workload.Processed();
    }
}, ct);

A long-running loop, where a heartbeat on every pass reveals a loop that is alive but stuck:

protected override async Task ExecuteAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await _client.SyncAsync(ct);
        workload.Beat();
        await Task.Delay(TimeSpan.FromMinutes(1), ct);
    }
}

A failure without an exception, such as a missing input file:

var run = workload.Start();
if (await _sftp.TryGetTodayStatementAsync() is not { } file)
{
    run.Fail("statement_missing", "No bank statement for today");
    return;
}
workload.Processed(await _importer.ImportAsync(file));
run.Succeed();

error.type is the exception's full type name, or the code passed to Fail. WorkloadRun is a struct, so starting a run does not allocate; ending it twice records it twice, so prefer RunAsync.

Hangfire

builder.Services.AddRunSignal().AddHangfire();   // registers recurring jobs at startup
builder.Services.AddHangfire((sp, config) => config
    .UseSqlServerStorage(connectionString)       // your storage
    .UseRunSignal(sp));                           // records every job run
  • What is recorded: for every job, success, failure (with the job's own exception type, unwrapped from Hangfire's wrapper), cancellation on shutdown and duration.
  • Naming: recurring jobs are named after their ID ("Nightly Invoice" becomes nightly-invoice), and their cron expression becomes the schedule. Other jobs are named after their class and method (InvoiceJobs.Generate becomes invoice-jobs.generate).
  • [Workload]: sets the name and expectations; count items through PerformContext:
[Workload("nightly-invoice", MaxDuration = "30m", MinProcessed = 100, Severity = Severity.Critical)]
public async Task GenerateInvoices(PerformContext? context = null)
{
    foreach (var order in await _orders.GetUnbilledAsync())
    {
        await _invoices.CreateAsync(order);
        context.GetWorkload()?.Processed();
    }
}

Durations accept 90s, 30m, 2h, 1d or hh:mm:ss. Options declared with AddWorkload for the same name take precedence over the attribute.

Quartz.NET

builder.Services.AddRunSignal().AddQuartz();     // registers scheduled jobs at startup

// Quartz.NET 3 (RunSignal.Quartz)
builder.Services.AddQuartz(q => q.AddJobListener<RunSignalJobListener>());

// Quartz.NET 4 (RunSignal.Quartz4, .NET 10): listeners take matchers
builder.Services.AddQuartz(q => q.AddJobListener<RunSignalJobListener>([GroupMatcher<JobKey>.AnyGroup()]));

Without dependency injection: scheduler.UseRunSignal(WorkloadRegistry.Shared) and await scheduler.DiscoverRunSignalWorkloadsAsync(WorkloadRegistry.Shared).

  • Naming: jobs are named after their key (reports.DailySales becomes reports.daily-sales; the DEFAULT group is left out). A job scheduled without an identity, whose key is a random GUID, is named after its class.
  • Schedules: a cron trigger's expression becomes the schedule. It is converted from Quartz's format (1 = Sunday) to the standard one.
  • [Workload] works on the job class, and context.GetWorkload()?.Processed() counts items.
  • Quartz.NET 4 changed its interfaces to ValueTask and supports only .NET 10. RunSignal.Quartz4 is built from the same sources and uses the same namespace, so upgrading only changes the package. It supports Quartz 4.0 and later and is tested against 4.6, including named schedulers.

Manifest

The application writes its manifest once it has started and discovered its jobs, when RUNSIGNAL_MANIFEST_PATH is set:

RUNSIGNAL_MANIFEST_PATH=artifacts/runsignal.json dotnet run

You can also set it in code: AddRunSignal(o => { o.ManifestPath = "runsignal.json"; o.ServiceName = "billing-service"; }). The service name defaults to OTEL_SERVICE_NAME, then the entry assembly name, and must match the service.name the signals are exported with. Without dependency injection, call registry.CreateManifest("billing-service").ToJson().

Exporting

New Relic needs delta temporality. With cumulative, a stopped workload never looks stopped:

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("billing-service"))      // must match the manifest's service
    .WithMetrics(m => m
        .AddRunSignalInstrumentation()
        .AddOtlpExporter((exporter, reader) =>
        {
            exporter.Endpoint = new Uri("https://otlp.eu01.nr-data.net");  // US: https://otlp.nr-data.net
            exporter.Headers = $"api-key={licenseKey}";                    // an ingest (license) key
            reader.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
        }));

Prometheus 3 receives OTLP at /api/v1/otlp/v1/metrics with the default (cumulative) temporality. Generate its rules with runsignal export prometheus.

Performance

Measured with BenchmarkDotNet on .NET 10 (Apple Silicon), with a metrics listener but without the OpenTelemetry SDK's own aggregation: dotnet run -c Release --project benchmarks/RunSignal.Benchmarks.

Operation Time Allocated
Start() + Succeed() ~35–40 ns 0 B
RunAsync (synchronously completing work) ~42 ns 0 B
Processed() ~1–6 ns 0 B
Beat() ~14 ns 0 B

Spans allocate only when a tracer samples them.

Samples

Specification · Changelog · Other languages

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on RunSignal.Core:

Package Downloads
RunSignal.DotNet

Registers RunSignal workloads with Microsoft.Extensions.DependencyInjection: AddRunSignal().AddWorkload("nightly-invoice", ...) and inject them with [FromKeyedServices]. Monitor background jobs, consumers and BackgroundService loops that no HTTP status code describes.

RunSignal.Alerting

Reads RunSignal workload manifests and computes when each alert should fire (missed runs from cron schedules, stalled heartbeats, overruns, low output, failure rates), shared by the New Relic and Prometheus backends so every backend alerts at the same moment.

RunSignal.OpenTelemetry

Adds RunSignal's workload metrics and spans to the OpenTelemetry .NET SDK with one call, with histogram buckets sized for jobs that run from milliseconds to hours. Send them to New Relic, Grafana, Datadog or any OTLP backend.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 83 10/29/2026
0.1.0 192 10/23/2026