Radiant 0.1.1

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

<p align="center"> <img src="https://raw.githubusercontent.com/jchristn/Radiant/main/assets/logo.png" alt="Radiant" width="170" height="192" /> </p>

Radiant

Telemetry for .NET without the wiring. Fill in one settings object, start one host, and your application's metrics, traces, and logs flow to Prometheus, Tempo, and Loki — or to any OpenTelemetry-compatible backend.

Status: alpha — v0.1.1. Radiant is early and under active development. The public API, defaults, and package layout may change between 0.x releases without notice. Pin a version if you depend on it, and read the CHANGELOG before upgrading.

What it is

Radiant is a small .NET library that owns the host side of telemetry — building the OpenTelemetry providers, configuring OTLP export, serving an in-process Prometheus endpoint, pushing logs to Loki, and shipping ready-made Grafana assets. You touch two types, RadiantSettings and RadiantHost, once at your application's composition root.

The deliberate decision is what Radiant doesn't do: it does not invent a way to emit telemetry. Emitting a measurement stays on the .NET base class library — Meter, ActivitySource, ILogger. A library that wants to be observable creates a Meter with a stable name and records into it. That costs nothing and throws nothing until an application decides to listen. Radiant is the listener — one consumer of the platform's telemetry primitives, not a framework you adopt end to end.

That split is the point. Emit rides the base class library, so any C# library — a web server, a cache, a driver — can produce telemetry without taking a dependency on Radiant. The application hosts, once. The two sides never reference each other; they meet at a string name.

What it does

  • Metrics — subscribes to your Meter instruments, exports them over OTLP, and optionally serves a Prometheus /metrics endpoint in-process. Ships baseline process and .NET runtime metrics.
  • Traces — subscribes to your ActivitySource spans with parent-based sampling and OTLP export, plus a thin RadiantSpan helper for timing units of work.
  • Logs — an ILogger pipeline exporting over OTLP, with direct-to-Loki support and trace/log correlation so a slow span links to that request's logs.
  • Conventions — an open Convention type and the OpenTelemetry semantic-convention catalog, so your instruments render in stock dashboards, plus an opt-in guardrail that enforces bounded metric cardinality.

Why use it

The value is concentrated on the hosting side — turning your instruments into an exported pipeline. Radiant deliberately doesn't reinvent how you emit; it removes the part that's genuinely tedious and easy to get wrong.

A working pipeline in a few lines instead of a few hundred. Standing up OpenTelemetry by hand means learning which of half a dozen packages you need, three separate provider builders, the OTLP exporter option matrix, resource attributes, samplers, the metric reader interval, the Prometheus listener, and Loki's exact OTLP path and tenant header — then getting flush-and-dispose ordering right. Radiant compresses all of it to a settings object with defaults that already work, while leaving the raw OpenTelemetry SDK accessible underneath when you need it. Time to first metric is minutes, not an afternoon.

Metrics on day one, with no infrastructure. Set Prometheus.Enable = true and the app serves /metrics in-process — you can curl it or point Prometheus straight at it before any collector exists. The collector becomes an upgrade, not a prerequisite.

The lifecycle correctness you'd otherwise get wrong by hand. One host per scrape port is guarded with a clear error instead of a buried socket exception; an invalid OTLP protocol fails fast rather than silently falling back; disposal flushes exporters and releases the port deterministically. These are exactly the mistakes hand-rolled telemetry makes.

Your libraries stay clean. Because emit is just the base class library, a foundational component can be fully instrumented and still impose nothing on the applications that use it — no Radiant dependency, no configuration, and no cost when telemetry is off. The library and the host meet at a name, not a reference.

Dashboards that light up, and a backend that won't fall over. Names follow the OpenTelemetry semantic conventions, so stock Grafana and vendor dashboards render without custom setup. And the one way a naive telemetry layer fails — an unbounded Record(anyString, value) surface that multiplies into millions of time series — is closed by construction: Radiant rides typed BCL instruments and offers an opt-in catalog that enforces bounded label cardinality, strict in Debug so mistakes surface on your machine.

Vendor-neutral, and testable. The same instruments Radiant reads are readable by dotnet-counters, dotnet-monitor, Application Insights, and any OpenTelemetry vendor — Grafana Cloud, Datadog, New Relic, Azure Monitor, CloudWatch — often several at once. And because emit rides the BCL, you can assert on metric values and labels in a unit test with an in-memory reader, no running host required.

One honest boundary: Radiant lowers the wiring cost, not the conceptual cost. You still decide what to measure, and you still need to understand metrics versus traces versus logs and why cardinality matters — it doesn't pretend those away.

Packages

Package What it is Depends on
Radiant The core: settings, host, convenience emitter, provider wiring OpenTelemetry SDK + exporters, Microsoft.Extensions.Logging
Radiant.SemConv Emit-side naming vocabulary: the Convention type + OTel semantic-convention definitions System.Diagnostics.DiagnosticSource only

The core carries no stack-specific dependencies. A library becomes observable by creating a Meter/ActivitySource with a stable name and emitting; Radiant subscribes to that name from the host side. A component that only needs to name its instruments consistently pulls in Radiant.SemConv and nothing else — not even the OpenTelemetry SDK. An application that wants the full pipeline pulls in Radiant.

Both packages multi-target netstandard2.0;netstandard2.1;net8.0;net10.0. The emit path is netstandard2.0-clean, so a down-level library can create a Meter, emit, and stay a no-op until a modern host subscribes.

Getting started

Install the core package:

dotnet add package Radiant

Start a host at your composition root. This is the only Radiant code most applications write:

using Radiant;

RadiantSettings settings = new RadiantSettings("orders-api");
settings.Otlp.Endpoint = "http://localhost:4317";      // your OpenTelemetry Collector
settings.Prometheus.Enable = true;                      // also serve /metrics in-process
settings.Sources.AddMeter("Orders.Domain");             // subscribe to your library's meter
settings.Sources.AddActivitySource("Orders.Domain");

using (RadiantHost host = RadiantHost.Start(settings))
{
    // ... run your application ...
}

That host builds the meter, tracer, and logging providers, subscribes to the sources you named, binds the Prometheus port, and flushes and releases everything on dispose. Nothing else is required to get metrics, traces, and logs leaving the process.

Emitting from library code (no Radiant dependency)

A library emits through the base class library. It never references Radiant:

using System.Diagnostics.Metrics;

public sealed class OrderProcessor
{
    private static readonly Meter _Meter = new Meter("Orders.Domain");
    private static readonly Counter<long> _Processed = _Meter.CreateCounter<long>("orders.processed");

    public void Process(Order order)
    {
        // ... work ...
        _Processed.Add(1);
    }
}

If no host ever subscribes to "Orders.Domain", that Add(1) is inert — no allocation that matters, no exception, no configuration. When an application does subscribe, the same counter lights up. That is exactly the behavior a foundational library needs when it may be embedded in an app that doesn't care about observability.

Emitting from application code (the convenience emitter)

If you would rather not manage Meter fields, the host exposes a Client that hands back cached instrument handles so a call site records against a handle, not a re-typed string:

host.Client.Increment("orders.processed", 1, new RadiantTag("region", "eu"));
host.Client.Record("orders.latency", elapsedSeconds, new RadiantTag("region", "eu"));
host.Client.RegisterGauge("orders.queue.depth", () => _queue.Count, "{item}");

Spans are a thin IDisposable over ActivitySource:

using (RadiantSpan span = host.StartSpan("process-order", SpanKindEnum.Server))
{
    span.SetTag("order.id", order.Id);
    try { Process(order); span.SetOk(); }
    catch (Exception e) { span.RecordException(e); throw; }
}

For the full end-to-end walkthrough — instrumenting a sample app from scratch, testing your instrumentation, and the performance cost of each instrument — see INTEGRATION.md.

Connecting to a collector or backend

Radiant can push over OTLP, serve Prometheus in-process, and ship logs to Loki — independently or all at once. Configure them on RadiantSettings.

OTLP push to a Collector. The default is gRPC on port 4317. For HTTP/protobuf, use port 4318 and set the protocol:

settings.Otlp.Endpoint = "http://collector:4318";
settings.Otlp.Protocol = OtlpProtocolEnum.HttpProtobuf;
settings.Otlp.TimeoutMs = 10000;
settings.Metrics.ExportIntervalMs = 15000;   // how often metrics are pushed

A hosted backend (Grafana Cloud, Honeycomb, etc.). Point the endpoint at the vendor's OTLP URL and add an auth header:

settings.Otlp.Endpoint = "https://otlp-gateway.example.grafana.net/otlp";
settings.Otlp.Protocol = OtlpProtocolEnum.HttpProtobuf;
settings.Otlp.Headers["Authorization"] = "Basic <base64-instance-and-token>";

In-process Prometheus scrape. When no Collector is deployed, serve /metrics from the app and point Prometheus straight at it — no collector required:

settings.Prometheus.Enable = true;
settings.Prometheus.Port = 9464;             // scrape at http://host:9464/metrics

OTLP push and the Prometheus endpoint can both run; the endpoint is a pull path, the OTLP exporter a push path, and they don't interfere.

Direct Loki export. Send logs to a Loki 3.x OTLP endpoint without a collector in the path:

settings.Loki.Enable = true;
settings.Loki.Endpoint = "http://loki:3100/otlp";
settings.Loki.TenantId = "team-orders";      // sent as X-Scope-OrgID

If your application already owns its ILoggingBuilder (a generic host, or one you hand to another sink), wire Radiant's log export in without going through RadiantHost:

builder.Logging.AddRadiant(settings);

Try it against a real Grafana

The docker/ directory holds a reference stack — Prometheus, Tempo, Loki, Grafana, and an OpenTelemetry Collector wired together, with datasources and an overview dashboard provisioned:

docker compose -f docker/compose.telemetry.yaml up -d

Then drive telemetry by hand with the console exerciser, or run the sample app:

dotnet run --project src/Radiant.Sdk.Console      # interactive: emit metrics/spans/logs, serve /metrics
dotnet run --project src/SampleApplication         # the INTEGRATION.md walkthrough, runnable

Open Grafana at http://localhost:3000 and watch the Radiant Overview dashboard fill in.

Names and conventions

The price of that cross-vendor usefulness is semantic-convention compliance: correct instrument kind, UCUM unit, and OTel-standard names and attributes. Radiant.SemConv ships those as reusable Convention definitions so the emitting library and the reading host agree by referencing the same value rather than re-typing strings:

host.Client.Record(SemConv.Http.RequestDuration, seconds,
    new RadiantTag(SemConv.Http.AttributeStatusCode, 200));

Convention is also the open extension point for your names. You declare custom instruments with the same factories, so built-in and custom are the same type flowing through the same emit and catalog APIs — nothing second-class about the ones you wrote:

public static readonly Convention Sorted =
    Convention.Counter("orders.processed", "{order}", "region", "outcome");

host.Client.Increment(Sorted, 1, new RadiantTag("region", "eu"));   // identical to the built-in call
settings.Metrics.DefineAll(Sorted, /* ... */);                       // register a whole set at once

A Convention converts implicitly to its name, so it also drops straight into a raw System.Diagnostics call — a netstandard2.0 library can reference Radiant.SemConv for the shared vocabulary without touching the OpenTelemetry SDK.

Configuration reference

RadiantSettings is the whole surface. Every pillar is on by default; numeric fields clamp to safe ranges rather than throwing.

Setting Default Notes
ServiceName required Stamped as service.name. Non-empty.
ServiceInstanceId auto GUID Stamped as service.instance.id.
Metrics.ExportIntervalMs 15000 OTLP metric reader cadence. Clamps 1000..300000.
Metrics.IncludeRuntime true .NET runtime instrumentation.
Metrics.LabelPolicy Auto Strict in Debug, lenient in Release.
Traces.SamplingRatio 1.0 Parent-based. Clamps 0..1.
Logs.MinimumSeverity 1 0 (verbose) .. 7 (drop all).
Otlp.Endpoint http://localhost:4317 Absolute URI.
Otlp.Protocol Grpc Or HttpProtobuf. Invalid values fail fast.
Prometheus.Enable false In-process scrape endpoint.
Prometheus.Port 9464 Clamps 1..65535. One host per port per process.
Loki.Enable false Direct OTLP-HTTP to Loki 3.x.

Documentation

  • INTEGRATION.md — the full walkthrough: instrument a sample app, connect it to Radiant, define conventions, test your instrumentation, and understand the performance cost.
  • CHANGELOG.md — what changed in each release.
  • docker/ — the reference Prometheus / Tempo / Loki / Grafana / Collector stack, with provisioned datasources and dashboard.
  • src/SampleApplication/ — the runnable companion to the walkthrough.

Building and testing

dotnet build src/Radiant.slnx -c Release
dotnet run  --project src/Test.Automated          # console runner, colored output
dotnet test src/Test.Xunit                         # same suites through xUnit
dotnet test src/Test.Nunit                         # same suites through NUnit

The suites live once in Test.Shared as runner-agnostic descriptors and execute through all three runners. They assert metric values and labels with in-memory readers, prove the catalog rejects undeclared labels, and confirm export reaches a stand-in OTLP/Loki endpoint.

Compatibility notes

The core executes on the app's real runtime (net8/net10) even though it publishes down-level assets. The OpenTelemetry package versions are pinned to ones that publish netstandard2.0 assets, and net7+ conveniences are guarded for the down-level targets. The in-process Prometheus endpoint depends on OpenTelemetry.Exporter.Prometheus.HttpListener, which the OpenTelemetry project ships only as a prerelease — the one prerelease dependency a stable Radiant carries, and a deliberate one.

Contributing, issues, and discussions

Bug reports and feature requests are welcome while Radiant is finding its shape. Because it's alpha, opening an issue before a large change is the fastest way to avoid rework.

  • File a bug or request a feature: open an issue.
  • Ask a question or float an idea: start a discussion.
  • Contribute code: fork, branch, and open a pull request against main. Please run the test suites (dotnet test src/Radiant.slnx) and match the existing code style.

License

Radiant is released under the MIT License. See LICENSE.md.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 49 8/1/2026
0.0.1 45 8/1/2026