# `BB.Diagnostic`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/diagnostic.ex#L5)

Diagnostic reporting for monitoring and awareness.

Diagnostics are separate from control errors - they provide observability
into system health without affecting control flow. Following the ROS2 model,
diagnostics use four levels that indicate component health status.

## Diagnostic Levels

- `:ok` - Component operating normally
- `:warn` - Unusual condition, but operation continues
- `:error` - Component has failed or is degraded
- `:stale` - No recent updates from component (timeout/disconnect)

## Usage

Publish diagnostics via telemetry:

    BB.Diagnostic.publish(
      component: [:robot, :arm, :elbow],
      level: :warn,
      message: "Motor temperature elevated",
      values: %{temperature: 65.2, threshold: 70.0}
    )

Subscribe to diagnostics in your application:

    :telemetry.attach(
      "my-diagnostic-handler",
      [:bb, :diagnostic],
      &MyApp.handle_diagnostic/4,
      nil
    )

## Integration with bb_liveview

The `bb_liveview` package provides a diagnostic dashboard that aggregates
and displays diagnostics from all robot components. It subscribes to the
`[:bb, :diagnostic]` telemetry event automatically.

## Separation from Control Errors

Diagnostics are for **awareness** - they inform operators about system state
but don't halt operations. Control errors (`BB.Error.*`) are for **control
flow** - they're returned from functions and affect program execution.

| Concern | Mechanism | Purpose |
|---------|-----------|---------|
| Diagnostics | Telemetry events | Monitoring dashboards |
| Control errors | Return values | Program control flow |

A component may publish a `:warn` diagnostic while continuing to operate,
or publish an `:error` diagnostic when it has failed. The safety system
(`BB.Safety`) handles critical failures independently.

# `level`

```elixir
@type level() :: :ok | :warn | :error | :stale
```

# `t`

```elixir
@type t() :: %BB.Diagnostic{
  component: [atom()],
  level: level(),
  message: String.t(),
  timestamp: DateTime.t(),
  values: map()
}
```

# `error`

```elixir
@spec error([atom()], String.t(), keyword()) :: :ok
```

Convenience function to publish an `:error` diagnostic.

## Examples

    BB.Diagnostic.error([:my_robot, :sensor], "Sensor disconnected",
      values: %{last_reading: ~U[2025-01-15 10:30:00Z]}
    )

# `new`

```elixir
@spec new(keyword()) :: t()
```

Creates a new diagnostic struct.

## Options

- `:component` (required) - Path to the component, e.g. `[:robot, :arm, :elbow]`
- `:level` (required) - One of `:ok`, `:warn`, `:error`, `:stale`
- `:message` (required) - Human-readable description
- `:values` - Map of diagnostic values (default: `%{}`)
- `:timestamp` - When the diagnostic was generated (default: `DateTime.utc_now/0`)

## Examples

    BB.Diagnostic.new(
      component: [:my_robot, :gripper],
      level: :ok,
      message: "Gripper operating normally"
    )

    BB.Diagnostic.new(
      component: [:my_robot, :arm, :shoulder],
      level: :warn,
      message: "Motor temperature elevated",
      values: %{temperature: 65.2, threshold: 70.0}
    )

# `ok`

```elixir
@spec ok([atom()], String.t(), keyword()) :: :ok
```

Convenience function to publish an `:ok` diagnostic.

## Examples

    BB.Diagnostic.ok([:my_robot, :arm], "Arm calibrated successfully")

    BB.Diagnostic.ok([:my_robot, :gripper], "Gripper ready",
      values: %{grip_force: 10.5}
    )

# `publish`

```elixir
@spec publish(t() | keyword()) :: :ok
```

Publishes a diagnostic event via telemetry.

This is the primary way to emit diagnostics. The event is published to
`[:bb, :diagnostic]` with the diagnostic struct as metadata.

Accepts either a `BB.Diagnostic` struct or keyword options (which are
passed to `new/1`).

## Examples

    # With keyword options
    BB.Diagnostic.publish(
      component: [:my_robot, :battery],
      level: :warn,
      message: "Battery low",
      values: %{percentage: 15, threshold: 20}
    )

    # With struct
    diagnostic = BB.Diagnostic.new(component: [:my_robot], level: :ok, message: "OK")
    BB.Diagnostic.publish(diagnostic)

# `stale`

```elixir
@spec stale([atom()], String.t(), keyword()) :: :ok
```

Convenience function to publish a `:stale` diagnostic.

Used when a component hasn't reported recently, indicating potential
disconnect or hang.

## Examples

    BB.Diagnostic.stale([:my_robot, :camera], "No frames received",
      values: %{last_frame: ~U[2025-01-15 10:25:00Z], timeout_ms: 5000}
    )

# `warn`

```elixir
@spec warn([atom()], String.t(), keyword()) :: :ok
```

Convenience function to publish a `:warn` diagnostic.

## Examples

    BB.Diagnostic.warn([:my_robot, :motor], "Temperature elevated",
      values: %{temperature: 65.0, threshold: 70.0}
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
