Class ValidationMetrics
- Namespace
- Trellis
- Assembly
- Trellis.Core.dll
The Trellis.ValidationMetrics.Meter and instruments Trellis publishes for validation failures.
public static class ValidationMetrics
- Inheritance
-
ValidationMetrics
- Inherited Members
Remarks
Validation failures look like user error, and that label is why nobody looks at them. The reason to count them is that server-side validation is a backstop: when a client enforces the same rules before sending, this counter should sit near zero. That expected value is what makes it worth alerting on — a rising count does not mean users got worse at typing, it means client-side validation has drifted from the server's, or a client broke against you, or you tightened a rule and did not notice. All three are your defect, arriving disguised as theirs.
The validation.code tag is what makes that actionable, and it is the reason an
HTTP-status metric is not a substitute: a 4xx rate tells you something drifted, the reason
code tells you which rule did. It deliberately stops there. There is no field or route
tag, because those are unbounded (see OtherCode for the same reasoning applied
to codes), so the division of labour is detection here and diagnosis in the trace, whose JSON
pointer names the offending field.
A second, narrower use is the framework's own: "is this rule dead?" — whether a reason code the framework can emit is ever actually emitted. Nothing else answers it. A trace answers it only for requests that were sampled, and a zero-volume code is indistinguishable from a code whose traces were all sampled away; a log answers it only if someone is aggregating the logs, which is the thing a counter is for. A code that never fires may also be shadowed by an earlier check rather than genuinely unused, which reads identically until you look.
A violation is counted where it is created. The counting site is the ReasonCode
initializer on FieldViolation and RuleViolation — the violation
itself, not the Error.InvalidInput that carries it. A failure can surface at the
HTTP boundary, at the mediator pipeline, at both, or — in a worker — at neither, so no
reporting site sees each failure exactly once. Coordinating the reporting sites does not work
either: an AsyncLocal set inside an awaited Send is not visible to the caller
afterwards, because a callee's assignment does not flow back up.
The carrying Error.InvalidInput is the wrong site for a subtler reason: it is
rebuilt during re-projection. JsonValidationPathRebase re-roots pointers by constructing
a fresh InvalidInput from an existing one's violations, and the ASP validation context
aggregates collected violations into a final one — so counting there would count the same rule
firing two or three times. The violation is the atom that is created once when a rule fires and
only ever copied afterwards.
A with-expression does not recount. The synthesized copy constructor copies backing
fields rather than re-running the initializers, which is what makes re-projection free: the
rebase path rewrites a violation's pointer with violation with { Field = ... } and the
count is unaffected.
Fields
FailuresInstrumentName
The name of the counter incremented once per violation carried by a validation failure.
public const string FailuresInstrumentName = "trellis.validation.failures"
Field Value
MeterName
The name of the Trellis.ValidationMetrics.Meter carrying Trellis validation instruments. Subscribe with AddTrellisValidationInstrumentation(MeterProviderBuilder).
public const string MeterName = "Trellis.Validation"
Field Value
OtherCode
The validation.code tag value substituted for any code outside the framework
vocabulary.
public const string OtherCode = "other"
Field Value
Remarks
An application code reaches the wire verbatim — ValidationCodeProjection passes
through anything it does not reserve — so an application is free to mint a code per entity,
per tenant, or per request. Tagging those verbatim would let a caller create an unbounded
number of time series, which is the expensive failure mode in a hosted metrics backend.
Framework codes are a closed set, and they are the only ones the dead-rule question is
about, so everything else is folded here. The total stays exact; only the breakdown is
bucketed.