Skip to content

Add transparent Count shorthand and document native Meter instrument reuse #36

Description

@droosma

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions