MailglassInbound.Telemetry (MailglassInbound v0.2.0)

Copy Markdown View Source

The single span surface for mailglass_inbound (D-45-01).

Mirrors Mailglass.Webhook.Telemetry: every inbound :telemetry.span/3 call lives here so the extended NoPiiInTelemetry check (enabled for inbound in Plan 01) has exactly ONE module to audit, plus the four call sites. Callers MUST NOT reach for :telemetry.span/3 directly — use the named helpers below.

Events emitted

EventTypeStop metadata keys (D-45-03 whitelist)
[:mailglass_inbound, :ingress, :request, :start | :stop | :exception]full spanprovider, tenant_id, status, byte_size
[:mailglass_inbound, :route, :match, :start | :stop | :exception]full spanmailbox, candidate_count, status
[:mailglass_inbound, :persist, :record, :start | :stop | :exception]full spanprovider, tenant_id, operation, record_type
[:mailglass_inbound, :execution, :run, :start | :stop | :exception]full spanmailbox, outcome, source
[:mailglass_inbound, :ingress, :rate_limit, :start | :stop | :exception]full spanprovider, tenant_id, bucket, limit, retry_after
[:mailglass_inbound, :ingress, :suppression_flag, :start | :stop | :exception]full spanprovider, tenant_id, flagged
[:mailglass_inbound, :prune, :sweep, :start | :stop | :exception]full spanstatus, records_deleted, evidence_deleted, fresh_runs_deleted, replay_runs_deleted

Event-name convention note (D-49-17 deviation): the CONTEXT named the rate-limit event [:mailglass_inbound, :rate_limit, :stop] and the prune event [:mailglass_inbound, :prune, :stop] (3 final segments). The locked [root, domain, resource, action] 4-segment telemetry convention (TelemetryEventConvention, enforced at lint time) requires 4 final segments, so these ship as [:mailglass_inbound, :ingress, :rate_limit, *] (rate-limit is an ingress-path event, beside :suppression_flag) and [:mailglass_inbound, :prune, :sweep, *]. Same resource name, convention-compliant.

Every helper is a full :start/:stop/:exception span via :telemetry.span/3. There is no single-emit (fire-and-forget) helper here — inbound emits via spans only. :telemetry.span/3 supplies :duration in its :stop measurements automatically; callers MUST NOT hand-compute latency into metadata.

Per-request stop metadata enrichment

Each helper accepts a zero-arity function returning either:

  • result — bare value; stop metadata equals the metadata argument passed at call time (before the outcome is known).
  • {result, stop_metadata} — tuple; stop metadata is the returned map. Used by the call sites to attach the classified :status, :operation, :outcome, :mailbox onto the :stop event after the inner function returns. Start metadata is always the metadata argument at call time.

Whitelist discipline (D-45-03)

The ONLY allowed metadata keys across all spans:

provider, tenant_id, status, latency, byte_size, mailbox, candidate_count,
outcome, source, operation, record_type,
bucket, limit, retry_after, flagged,
records_deleted, evidence_deleted, fresh_runs_deleted, replay_runs_deleted

The Phase-49 additions (bucket, limit, retry_after, flagged + the per-table prune counts) are all counts/types/statuses — never PII (D-49-17/23/29). The rate-limit + suppression-flag spans carry the bucket TYPE and a boolean flag, never the recipient/sender value.

NEVER include in any metadata map:

:to, :from, :cc, :bcc, :subject, :body, :html_body, :headers,
:recipient, :sender, :email

NoPiiInTelemetry (extended to inbound in Plan 01) lints THIS module plus every caller against the forbidden-key set.

Handler isolation (TELE-05)

:telemetry.span/3 wraps each attached handler in a try/catch. A handler that raises is detached automatically and [:telemetry, :handler, :failure] is emitted — the caller's inbound pipeline is unaffected. mailglass_inbound does not add a parallel try/rescue around business code (that would duplicate or, worse, swallow the meta-event operators rely on). TELE-05 comes for free from routing every span through :telemetry.span/3.

Summary

Functions

Wrap the body of MailglassInbound.Execution.execute/2 in a [:mailglass_inbound, :execution, :run, *] span.

Wrap the entire inbound ingress path in a [:mailglass_inbound, :ingress, :request, *] span.

Wrap the repo.transact in MailglassInbound.Ingress.Persist.persist/2 in a [:mailglass_inbound, :persist, :record, *] span.

Wrap a retention prune sweep in a [:mailglass_inbound, :prune, :sweep, *] span (IOPS-03, D-49-29). Consumed by Plan 03. The :sweep resource segment satisfies the 4-segment event convention.

Wrap the post-verify rate-limit check in a [:mailglass_inbound, :ingress, :rate_limit, *] span (IOPS-04, D-49-17). The rate limiter is an ingress-path event, so it lives under the :ingress domain (beside :suppression_flag) to satisfy the 4-segment event convention.

Wrap MailglassInbound.Router.Matcher.match/2 in a [:mailglass_inbound, :route, :match, *] span.

Wrap the inbound suppression-flag computation in a [:mailglass_inbound, :ingress, :suppression_flag, *] span (IOPS-05, D-49-23). Consumed by Plan 02.

Functions

execution_span(metadata, fun)

(since 0.2.0)
@spec execution_span(map(), (-> result | {result, map()})) :: result
when result: term()

Wrap the body of MailglassInbound.Execution.execute/2 in a [:mailglass_inbound, :execution, :run, *] span.

This is the single synchronous sync point both the Oban and Task.Supervisor async paths funnel through, so the span covers both (D-45-02, RESEARCH Pitfall 5 — wrap execute/2, never dispatch/2).

Stop metadata SHOULD include :mailbox, :outcome, :source. All PII-free.

fun may return a bare result OR {result, stop_metadata}.

ingress_span(metadata, fun)

(since 0.2.0)
@spec ingress_span(map(), (-> result | {result, map()})) :: result when result: term()

Wrap the entire inbound ingress path in a [:mailglass_inbound, :ingress, :request, *] span.

Stop metadata SHOULD include :provider, :tenant_id, :status, :byte_size. NEVER include PII (see the module doc whitelist). Latency is supplied by :telemetry.span/3 in its measurements — do not put it in metadata.

fun may return a bare result OR {result, stop_metadata} — see the moduledoc's "Per-request stop metadata enrichment" section.

persist_span(metadata, fun)

(since 0.2.0)
@spec persist_span(map(), (-> result | {result, map()})) :: result when result: term()

Wrap the repo.transact in MailglassInbound.Ingress.Persist.persist/2 in a [:mailglass_inbound, :persist, :record, *] span.

Stop metadata SHOULD include :provider, :tenant_id, :operation (:insert | :dedup_skip), :record_type. All PII-free.

fun may return a bare result OR {result, stop_metadata}.

prune(metadata, fun)

(since 1.2.0)
@spec prune(map(), (-> result | {result, map()})) :: result when result: term()

Wrap a retention prune sweep in a [:mailglass_inbound, :prune, :sweep, *] span (IOPS-03, D-49-29). Consumed by Plan 03. The :sweep resource segment satisfies the 4-segment event convention.

Stop metadata SHOULD include the per-table counts :records_deleted, :evidence_deleted, :fresh_runs_deleted, :replay_runs_deleted, and a :status. Counts only — no PII.

fun may return a bare result OR {result, stop_metadata}.

rate_limit(metadata, fun)

(since 1.2.0)
@spec rate_limit(map(), (-> result | {result, map()})) :: result when result: term()

Wrap the post-verify rate-limit check in a [:mailglass_inbound, :ingress, :rate_limit, *] span (IOPS-04, D-49-17). The rate limiter is an ingress-path event, so it lives under the :ingress domain (beside :suppression_flag) to satisfy the 4-segment event convention.

Stop metadata SHOULD include :provider, :tenant_id, and on a trip the bucket :bucket TYPE (:tenant | :recipient | :sender_domain), :limit (capacity), and :retry_after (seconds). It MUST NOT carry the recipient or sender VALUE — only the bucket type (D-49-16).

fun may return a bare result OR {result, stop_metadata}.

route_span(metadata, fun)

(since 0.2.0)
@spec route_span(map(), (-> result | {result, map()})) :: result when result: term()

Wrap MailglassInbound.Router.Matcher.match/2 in a [:mailglass_inbound, :route, :match, *] span.

Stop metadata SHOULD include :mailbox + :candidate_count on a match, or :status (:no_match) + :candidate_count on a miss. All PII-free.

fun may return a bare result OR {result, stop_metadata}.

suppression_flag(metadata, fun)

(since 1.2.0)
@spec suppression_flag(map(), (-> result | {result, map()})) :: result
when result: term()

Wrap the inbound suppression-flag computation in a [:mailglass_inbound, :ingress, :suppression_flag, *] span (IOPS-05, D-49-23). Consumed by Plan 02.

Stop metadata SHOULD include :flagged (boolean), :tenant_id, :provider — never the address. No auto-bounce, no auto-suppression: the flag is diagnostic signal only.

fun may return a bare result OR {result, stop_metadata}.