RunSignal.DotNet 0.2.0

dotnet add package RunSignal.DotNet --version 0.2.0
                    
NuGet\Install-Package RunSignal.DotNet -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.DotNet" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RunSignal.DotNet" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="RunSignal.DotNet" />
                    
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.DotNet --version 0.2.0
                    
#r "nuget: RunSignal.DotNet, 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.DotNet@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.DotNet&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=RunSignal.DotNet&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.

NuGet packages (3)

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

Package Downloads
RunSignal.Quartz

Records every Quartz.NET job execution as a RunSignal workload run (success, failure, cancellation and duration) with no code in your jobs. Cron trigger schedules are picked up for missed-run alerts.

RunSignal.Hangfire

Records every Hangfire job run as a RunSignal workload run (success, failure, cancellation and duration) with no code in your jobs. Recurring jobs are named after their ID and their cron schedule is picked up for missed-run alerts.

RunSignal.Quartz4

Records every Quartz.NET 4 job execution as a RunSignal workload run (success, failure, cancellation and duration) with no code in your jobs. Cron trigger schedules are picked up for missed-run alerts. For Quartz.NET 3, use RunSignal.Quartz.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 74 10/29/2026
0.1.0 141 10/23/2026