EtherCAT.Telemetry (ethercat v0.5.0)

Copy Markdown View Source

Telemetry integration for the EtherCAT stack.

Emits events via :telemetry and provides lightweight atomic counters for IEx inspection.

Introspection Strategy

Telemetry is the machine-readable surface:

  • event names are stable and enumerable through events/0
  • measurements are numeric
  • metadata is intentionally low-cardinality and bounded
  • detailed failure terms stay in logs and API replies, not telemetry
  • the [:ethercat, :bus, :transact, :exception] span event is the one intentional exception and keeps the raw exception payload from :telemetry.span/3

Logs are the human-readable narrative:

  • info for lifecycle transitions and successful recovery
  • warning for degraded but recoverable runtime conditions
  • error for crashes and session-stopping faults
  • debug for step-by-step progress and repeated retry chatter

This split keeps dashboards and alerts predictable while preserving rich operator context in the logs.

Events

Transaction span (emitted by Bus.transaction/2|3)

[:ethercat, :bus, :transact, :start]
  measurements: %{system_time: integer(), monotonic_time: integer()}
  metadata:     %{datagram_count: integer(), class: :realtime | :reliable}

[:ethercat, :bus, :transact, :stop]
  measurements: %{duration: integer()}
  metadata:     %{datagram_count: integer(), total_wkc: integer(), class: :realtime | :reliable, status: :ok | :error, error_kind: atom() | nil}

[:ethercat, :bus, :transact, :exception]
  measurements: %{duration: integer()}
  metadata:     %{kind: atom(), reason: term(), stacktrace: list()}

Submission events

[:ethercat, :bus, :submission, :enqueued]
  measurements: %{queue_depth: non_neg_integer()}
  metadata:     %{link: String.t(), class: :realtime | :reliable, state: :idle | :awaiting}

queue_depth is the depth of that submission class immediately after admission and before any immediate dispatch on an idle bus.

[:ethercat, :bus, :submission, :expired]
  measurements: %{age_us: non_neg_integer()}
  metadata:     %{link: String.t(), class: :realtime}

Dispatch events

[:ethercat, :bus, :dispatch, :sent]
  measurements: %{transaction_count: pos_integer(), datagram_count: pos_integer()}
  metadata:     %{link: String.t(), class: :realtime | :reliable}

Frame events

[:ethercat, :bus, :frame, :sent]
  measurements: %{size: integer(), tx_timestamp: integer() | nil}
  metadata:     %{link: String.t(), endpoint: String.t(), port: :primary | :secondary}

[:ethercat, :bus, :frame, :received]
  measurements: %{size: integer(), rx_timestamp: integer() | nil}
  metadata:     %{link: String.t(), endpoint: String.t(), port: :primary | :secondary}

[:ethercat, :bus, :frame, :dropped]
  measurements: %{size: integer()}
  metadata:     %{link: String.t(), reason: atom()}
[:ethercat, :bus, :link, :down]
  measurements: %{}
  metadata:     %{link: String.t(), endpoint: String.t(), reason: atom()}

[:ethercat, :bus, :link, :reconnected]
  measurements: %{}
  metadata:     %{link: String.t(), endpoint: String.t()}

[:ethercat, :bus, :link, :health, :changed]
  measurements: %{}
  metadata:     %{link: String.t(), port: :primary | :secondary, from: term(), to: term()}

[:ethercat, :bus, :link, :exchange, :timeout]
  measurements: %{arrival_count: non_neg_integer(), consecutive_timeouts: pos_integer()}
  metadata:     %{link: String.t(), detail: :no_arrivals | :partial_arrivals, arrival_classes: [atom()], pri_sent: boolean(), sec_sent: boolean()}

DC maintenance and lock monitoring

[:ethercat, :dc, :tick]
  measurements: %{wkc: integer()}
  metadata:     %{ref_station: non_neg_integer()}

[:ethercat, :dc, :sync_diff, :observed]
  measurements: %{max_sync_diff_ns: non_neg_integer()}
  metadata:     %{ref_station: non_neg_integer(), station_count: pos_integer()}

[:ethercat, :dc, :lock, :changed]
  measurements: %{}
  metadata:     %{ref_station: non_neg_integer(), from: atom(), to: atom(), max_sync_diff_ns: non_neg_integer() | nil}

[:ethercat, :dc, :runtime, :state, :changed]
  measurements: %{}
  metadata:     %{from: :healthy | :failing, to: :healthy | :failing, reason: atom() | nil, consecutive_failures: non_neg_integer()}

Domain cycle events

[:ethercat, :domain, :cycle, :done]
  measurements: %{duration_us: integer(), cycle_count: non_neg_integer(), completed_at_us: integer()}
  metadata:     %{domain: atom()}

[:ethercat, :domain, :cycle, :invalid]
  measurements: %{total_invalid_count: pos_integer(), invalid_at_us: integer()}
  metadata:     %{domain: atom(), reason: atom(), expected_wkc: non_neg_integer() | nil, actual_wkc: non_neg_integer() | nil, reply_count: non_neg_integer() | nil}

[:ethercat, :domain, :cycle, :transport_miss]
  measurements: %{consecutive_miss_count: pos_integer(), total_invalid_count: pos_integer(), invalid_at_us: integer()}
  metadata:     %{domain: atom(), reason: atom(), expected_wkc: non_neg_integer() | nil, actual_wkc: non_neg_integer() | nil, reply_count: non_neg_integer() | nil}

Domain fault events

[:ethercat, :domain, :stopped]
  measurements: %{}
  metadata:     %{domain: atom(), reason: atom()}

[:ethercat, :domain, :crashed]
  measurements: %{}
  metadata:     %{domain: atom(), reason: atom()}

Master lifecycle events

[:ethercat, :master, :state, :changed]
  measurements: %{}
  metadata:     %{from: atom(), to: atom(), public_state: atom(), runtime_target: atom()}

[:ethercat, :master, :startup, :bus_stable]
  measurements: %{slave_count: non_neg_integer()}
  metadata:     %{}

[:ethercat, :master, :configuration, :result]
  measurements: %{duration_ms: non_neg_integer()}
  metadata:     %{status: :ok | :error, slave_count: non_neg_integer(), runtime_target: atom(), reason: atom() | nil}

[:ethercat, :master, :activation, :result]
  measurements: %{duration_ms: non_neg_integer()}
  metadata:     %{status: :ok | :blocked | :error, runtime_target: atom(), blocked_count: non_neg_integer(), reason: atom() | nil}

[:ethercat, :master, :dc_lock, :decision]
  measurements: %{}
  metadata:     %{transition: :lost | :regained, policy: atom(), outcome: atom(), lock_state: atom(), max_sync_diff_ns: non_neg_integer() | nil}

[:ethercat, :master, :slave_fault, :changed]
  measurements: %{}
  metadata:     %{slave: atom(), from: atom() | nil, to: atom() | nil, from_detail: atom() | nil, to_detail: atom() | nil}

Slave fault events

[:ethercat, :slave, :crashed]
  measurements: %{}
  metadata:     %{slave: atom(), reason: atom()}

[:ethercat, :slave, :health, :fault]
  measurements: %{al_state: 1 | 2 | 4 | 8, error_code: non_neg_integer()}
  metadata:     %{slave: atom(), station: non_neg_integer()}

[:ethercat, :slave, :down]
  measurements: %{}
  metadata:     %{slave: atom(), station: non_neg_integer(), reason: atom()}

[:ethercat, :slave, :startup, :retry]
  measurements: %{retry_count: pos_integer(), retry_delay_ms: pos_integer()}
  metadata:     %{slave: atom(), station: non_neg_integer(), phase: atom(), reason: atom()}

Timestamps

tx_timestamp and rx_timestamp are System.monotonic_time/0 values. When Linux SO_TIMESTAMPING is available, rx_timestamp is derived from the kernel's software RX timestamp for nanosecond accuracy. Their difference gives wire-level round-trip time.

Example

:telemetry.attach_many("ethercat-log", [
  [:ethercat, :bus, :transact, :stop],
  [:ethercat, :bus, :link, :down]
], &MyHandler.handle_event/4, nil)

Summary

Functions

Attach counters to all EtherCAT telemetry events.

Detach counters.

Return the canonical list of public EtherCAT telemetry events.

Reset all counters to zero.

Return current counters as {event, count} tuples.

Print event counts.

Functions

attach()

Attach counters to all EtherCAT telemetry events.

Use stats/0 to print current counts and reset/0 to zero them.

Example

EtherCAT.Telemetry.attach()
# ... do some work ...
EtherCAT.Telemetry.stats()
EtherCAT.Telemetry.reset()

detach()

Detach counters.

events()

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

Return the canonical list of public EtherCAT telemetry events.

This is the supported source of truth for external consumers that want to subscribe via :telemetry.attach_many/4 without duplicating event names.

Example

:ok =
  :telemetry.attach_many(
    "ethercat-dashboard",
    EtherCAT.Telemetry.events(),
    &MyDashboard.handle_event/4,
    nil
  )

reset()

Reset all counters to zero.

snapshot()

@spec snapshot() :: [{[atom()], non_neg_integer()}]

Return current counters as {event, count} tuples.

stats()

Print event counts.