Exgit.Telemetry (exgit v0.1.0)

Copy Markdown View Source

Telemetry event names and conventions for exgit.

SemVer

This module — including the event names and the metadata key sets documented below — is part of exgit's public API under SemVer. A major-version bump is required to rename an event or remove a documented metadata key. New metadata keys may be added in any release.

Span convention

All events follow the :telemetry.span/3 convention, so each logical operation emits three events:

  • [..., :start] — measurements: %{system_time, monotonic_time}
  • [..., :stop] — measurements: %{duration, monotonic_time}
  • [..., :exception] — when the operation raised

The duration is in :native time units — convert with System.convert_time_unit(duration, :native, :microsecond).

Consumers attach to these events via :telemetry.attach/4. Dev/test environments can bridge to OpenTelemetry via opentelemetry_telemetry; see test/support/otel_console.ex for an example.

Event catalogue

Transport

  • [:exgit, :transport, :fetch]%{transport, url, wants_count, result_bytes, object_count}
  • [:exgit, :transport, :ls_refs]%{transport, url, ref_count}
  • [:exgit, :transport, :push]%{transport, url, update_count, pack_bytes}

Object store

  • [:exgit, :object_store, :get]%{store, sha, hit?}
    • hit?: true when a Promisor-backed store served from cache
    • hit?: false when it had to go to the network
    • omitted for stores that don't track this distinction
  • [:exgit, :object_store, :put]%{store, sha}
  • [:exgit, :object_store, :object_size]%{store, sha, hit?}
    • hit?: false covers both {:error, :not_found} and {:error, :not_local} (a Promisor never fetches for a size probe)
  • [:exgit, :object_store, :has?]%{store, sha, present?}
  • [:exgit, :object_store, :fetch_and_cache]%{sha, object_count, cache_bytes} (stop event only)
  • [:exgit, :object_store, :haves_sent] — standalone (non-span) event; measurements: %{count}; metadata: %{sha}
  • [:exgit, :object_store, :cache_overfull] — standalone; measurements: %{bytes, cap}; metadata: %{policy} where policy is :log | :error | :callback. Fired when a Promisor's eviction loop can't reduce cache_bytes below max_cache_bytes (commit queue empty, only blobs/trees left).
  • [:exgit, :object_store, :shared_promisor, :resolve] — span; metadata: %{sha, hit?, partial?}. hit?: true means the object was served from cache (promisor unchanged); partial?: true means the fetch returned a pack but the requested SHA wasn't in it (sibling objects were still cached).
  • [:exgit, :object_store, :shared_promisor, :put] — span; metadata: %{sha, overfull: boolean}.
  • [:exgit, :object_store, :shared_promisor, :get] — span; metadata: %{sha, hit?}.
  • [:exgit, :object_store, :shared_promisor, :has?] — span; metadata: %{sha, present?}.

Ref store

  • [:exgit, :ref_store, :write_failed] — standalone; measurements: %{count}; metadata: %{ref, reason, context}. Fired when Exgit.clone/2 with lazy: true (or filter:) encounters a ref-store write failure while seeding the memory-backed ref store.

Pack

  • [:exgit, :pack, :parse]%{byte_size, object_count}
  • [:exgit, :pack, :stream_parse] — span; metadata: %{objects, object_count, checksum} on success, %{objects, error} on failure. Fires when the HTTP transport's streaming parser path runs — i.e., Transport.fetch was called with object_store: in opts so pack bytes are decoded straight into the store without buffering the full pack in memory.

FS

  • [:exgit, :fs, :read_path]%{reference, path}
  • [:exgit, :fs, :ls]%{reference, path, entry_count}
  • [:exgit, :fs, :stat]%{reference, path}
  • [:exgit, :fs, :walk]%{reference, file_count} (stop only)
  • [:exgit, :fs, :grep]%{reference, pattern, path_glob, match_count, files_scanned, bytes_scanned} (stop only)

Security

  • [:exgit, :security, :ref_rejected] — standalone; measurements: %{count}; metadata: %{source, ref}. source is either a URL string (wire-layer rejection) or a {:ref_store_disk, operation} tuple (defense-in-depth rejection). Fired when a ref name fails Exgit.RefName.valid?/1.

Summary

Functions

Canonical list of non-span events exgit emits.

Canonical list of span-event prefixes exgit emits.

Wrap an operation with telemetry span events.

Run fun with a caller-supplied telemetry handler attached to every event in events/0. Handles attach / detach around the call; returns whatever fun returns.

Functions

emit_events()

@spec emit_events() :: [[atom()]]

Canonical list of non-span events exgit emits.

Unlike events/0 (which returns prefixes that get :start / :stop / :exception suffixes appended by :telemetry.span), these events fire once with :telemetry.execute/3. They represent discrete occurrences — "the batched prefetch path was abandoned and we fell back to the slow path" is a thing that happens once, not a span of time.

Part of the public API under SemVer (same rules as events/0).

events()

@spec events() :: [[atom()]]

Canonical list of span-event prefixes exgit emits.

Returned as a list of 2- or 3-element atom lists without the :start / :stop / :exception suffix. Callers who want to attach to the stop event append :stop themselves; callers who want all three use :telemetry.span's start/stop/exception suffixes.

Part of the public API under SemVer — removing an event name or renaming one is a major-version bump. Adding new event names is not breaking.

Example

stops = for e <- Exgit.Telemetry.events(), do: e ++ [:stop]

:telemetry.attach_many("my-handler", stops, fn event, m, md, _ ->
  IO.inspect({event, m.duration, md})
end, nil)

span(event, metadata, fun)

@spec span([atom()], map(), (-> {:span, result, map()} | result)) :: result
when result: var

Wrap an operation with telemetry span events.

The callable MUST return one of two shapes:

  • A tagged tuple {:span, result, extra_metadata} — the result is returned to the caller unchanged; extra_metadata (a map) is merged into the :stop event's metadata.
  • Anything else — returned verbatim; :stop event metadata equals the metadata passed in.

Using a tagged :span triple avoids ambiguity with library functions that naturally return {:ok, map()} 2-tuples (e.g. push/4 returning {:ok, %{ref_results: _}}).

with_handler(handler_fun, fun)

@spec with_handler(:telemetry.handler_function(), (-> result)) :: result
when result: var

Run fun with a caller-supplied telemetry handler attached to every event in events/0. Handles attach / detach around the call; returns whatever fun returns.

The caller owns the aggregation policy. We just provide the plumbing.

Handler signature

Standard :telemetry handler: (event_name, measurements, metadata, config) -> any. Runs in the process that fired the event.

Examples

Dead-simple "print every event":

Exgit.Telemetry.with_handler(
  fn event, m, md, _ -> IO.inspect({event, m.duration, md}) end,
  fn ->
    {:ok, repo} = Exgit.clone(url, lazy: true)
    Exgit.FS.grep(repo, "HEAD", "foo") |> Enum.to_list()
  end
)

Aggregate into per-event totals in ETS:

table = :ets.new(:my_totals, [:public, :bag])

Exgit.Telemetry.with_handler(
  fn event, %{duration: d}, _md, _ ->
    :ets.insert(table, {event, d})
  end,
  fn -> my_agent_step() end
)

:ets.tab2list(table)
# => [{[:exgit, :fs, :grep, :stop], 11_340_000}, ...]

Bridge to OpenTelemetry:

Exgit.Telemetry.with_handler(
  &OpentelemetryTelemetry.handle_event/4,
  fn -> my_agent_step() end
)

Scope

Attaches to :stop events only. Callers who want start events too should attach manually via :telemetry.attach_many/4 and events/0.