Summary
Add a fire-and-forget metric recording API such as:
witness.Count("metric_exporter.runs");
witness.Count("metric_exporter.source_rows", sourceRows);
witness.Count("metric_exporter.runs", 1, new("outcome", "Succeeded"));
The consumer should not need to keep a Counter<T> field or think about instrument lifetime. Repeated and concurrent calls for the same instrument definition must reuse the same long-lived instrument.
This is a follow-up to #1.
Important existing behavior
The caching part is already present at the correct layer:
AddWitness() registers one shared Meter as a singleton.
- .NET's
Meter.CreateCounter<T>(...) maintains its own thread-safe instrument registry.
- Repeating an identical instrument creation on the same
Meter returns the existing instrument.
- WitnessSharp's existing
witness.Counter<T>(name) delegates to Meter.CreateCounter<T>(name) and therefore already gets this reuse.
Consequently, WitnessSharp should not add a second dictionary cache merely to retain instrument objects. The missing feature is a transparent recording convenience and, optionally, explicit setup/policy that makes the reuse contract clear to consumers.
Current safe usage:
witness.Counter<long>("metric_exporter.runs").Add(1);
Desired concise usage:
witness.Count("metric_exporter.runs");
Both should use the same underlying long-lived instrument.
Proposed API direction
Add extensions on IWitness that delegate to the existing cached accessor:
public static void Count(this IWitness witness, string name)
=> witness.Counter<long>(name).Add(1);
public static void Count(this IWitness witness, string name, long value)
=> witness.Counter<long>(name).Add(value);
public static void Count(
this IWitness witness,
string name,
long value,
params ReadOnlySpan<KeyValuePair<string, object?>> tags)
=> witness.Counter<long>(name).Add(value, tags);
Exact overloads should follow the instrument APIs available on both supported target frameworks. Generic numeric overloads may be useful, but a simple long counter covers the common event-count case and avoids excessive API surface.
Potential matching histogram convenience:
witness.Observe("metric_exporter.duration", elapsed.TotalMilliseconds);
That can be a separate issue if it makes this one too broad.
Instrument identity and conflicting metadata
The reuse contract needs to be explicit about the instrument key. It is not just the metric name in every situation. Instrument kind, measurement type, unit, description, and advice can affect whether two declarations are compatible.
A convenience method must not silently turn these into the same instrument:
witness.Count("request.duration");
witness.Histogram<double>("request.duration", unit: "ms");
Likewise, whichever call creates an instrument first should not make later unit/description declarations surprising.
Possible policy:
Count(name, ...) always means Counter<long> with no unit or description.
- Advanced metadata requires obtaining the real instrument through
Counter<T>(...) and retaining a consistent declaration.
- Conflicting declarations retain the .NET
Meter behavior rather than introducing WitnessSharp-specific shadow caching.
Optional explicit IoC setup
Transparent behavior should be the default because the shared Meter already supplies caching. Still, an explicit setup API could make the behavior discoverable and provide a future home for diagnostics or validation:
services.AddWitness(options => ...)
.WithInstrumentReuse();
This should not create another cache if it adds no semantics beyond Meter. It would only be worthwhile if it enables behavior such as:
- detecting conflicting name/kind/type/unit declarations early;
- recording cache/reuse diagnostics;
- enforcing an instrument naming policy;
- exposing a registry for troubleshooting or tests.
An alternative is to document that instrument reuse is always enabled by the singleton Meter and avoid an IoC method that implies WitnessSharp owns a second cache.
Acceptance criteria
witness.Count("name") increments a Counter<long> by one.
- Repeated calls reuse the same instrument supplied by the shared
Meter.
- Concurrent calls are safe.
- Tags are supported without allocating an unnecessary intermediary collection where the target framework APIs permit it.
- The behavior composes with
witness.Counter<long>("name").Add(...).
- Conflicting instrument definitions are not silently merged.
TestWitness<T> captures measurements made through Count(...).
- Documentation explains that .NET
Meter owns the underlying instrument cache and WitnessSharp provides transparent access to it.
- A test verifies reference reuse for identical
Counter<T> declarations and metric capture through the new shorthand.
Motivation
While dogfooding WitnessSharp in a small telemetry exporter, a dedicated class was introduced solely because the consumer believed counters and histograms had to be retained manually. The native Meter registry and existing Counter<T> extension already solve the lifetime problem, but the API and docs did not make that obvious. A concise recording method plus explicit documentation would let consumers stay entirely within the Witness abstraction without inventing their own observability facade or instrument cache.
Summary
Add a fire-and-forget metric recording API such as:
The consumer should not need to keep a
Counter<T>field or think about instrument lifetime. Repeated and concurrent calls for the same instrument definition must reuse the same long-lived instrument.This is a follow-up to #1.
Important existing behavior
The caching part is already present at the correct layer:
AddWitness()registers one sharedMeteras a singleton.Meter.CreateCounter<T>(...)maintains its own thread-safe instrument registry.Meterreturns the existing instrument.witness.Counter<T>(name)delegates toMeter.CreateCounter<T>(name)and therefore already gets this reuse.Consequently, WitnessSharp should not add a second dictionary cache merely to retain instrument objects. The missing feature is a transparent recording convenience and, optionally, explicit setup/policy that makes the reuse contract clear to consumers.
Current safe usage:
Desired concise usage:
Both should use the same underlying long-lived instrument.
Proposed API direction
Add extensions on
IWitnessthat delegate to the existing cached accessor:Exact overloads should follow the instrument APIs available on both supported target frameworks. Generic numeric overloads may be useful, but a simple
longcounter covers the common event-count case and avoids excessive API surface.Potential matching histogram convenience:
That can be a separate issue if it makes this one too broad.
Instrument identity and conflicting metadata
The reuse contract needs to be explicit about the instrument key. It is not just the metric name in every situation. Instrument kind, measurement type, unit, description, and advice can affect whether two declarations are compatible.
A convenience method must not silently turn these into the same instrument:
Likewise, whichever call creates an instrument first should not make later unit/description declarations surprising.
Possible policy:
Count(name, ...)always meansCounter<long>with no unit or description.Counter<T>(...)and retaining a consistent declaration.Meterbehavior rather than introducing WitnessSharp-specific shadow caching.Optional explicit IoC setup
Transparent behavior should be the default because the shared
Meteralready supplies caching. Still, an explicit setup API could make the behavior discoverable and provide a future home for diagnostics or validation:This should not create another cache if it adds no semantics beyond
Meter. It would only be worthwhile if it enables behavior such as:An alternative is to document that instrument reuse is always enabled by the singleton
Meterand avoid an IoC method that implies WitnessSharp owns a second cache.Acceptance criteria
witness.Count("name")increments aCounter<long>by one.Meter.witness.Counter<long>("name").Add(...).TestWitness<T>captures measurements made throughCount(...).Meterowns the underlying instrument cache and WitnessSharp provides transparent access to it.Counter<T>declarations and metric capture through the new shorthand.Motivation
While dogfooding WitnessSharp in a small telemetry exporter, a dedicated class was introduced solely because the consumer believed counters and histograms had to be retained manually. The native
Meterregistry and existingCounter<T>extension already solve the lifetime problem, but the API and docs did not make that obvious. A concise recording method plus explicit documentation would let consumers stay entirely within the Witness abstraction without inventing their own observability facade or instrument cache.