---
plan: 02-01
title: "Junction + fill_from_rest telemetry instrumentation"
phase: 2
wave: 1
depends_on: []
files_modified:
  - crates/opendeviationbar-streaming/src/live_engine.rs
requirements:
  - TELE-01
  - TELE-02
  - TELE-03
  - TELE-04
  - TELE-05
  - AUDT-02
  - AUDT-03
  - AUDT-04
  - AUDT-05
autonomous: true
estimated_lines: ~25
---

# Plan 02-01: Junction + fill_from_rest Telemetry Instrumentation

## Goal

Add structured tracing fields to three sites in `live_engine.rs` so that journal output contains all telemetry needed to validate Phase 1 and tune Phase 4.

## Must-Haves (Goal-Backward)

1. Every symbol at startup emits: `junction_gap_tid_delta`, `forming_bar_trades`, `ws_buffer_depth`, `puck_event_type`
2. `fill_from_rest` emits per-symbol summary: `{symbol, duration_ms, trade_count, bars_produced}`
3. Puck events carry `event_type` field: `"junction_suppressed"` or `"normal_gap_fill"`

## Tasks

### Task 1: Per-symbol fill_from_rest summary with timing (TELE-05, AUDT-02)

<read_first>

- `crates/opendeviationbar-streaming/src/live_engine.rs` lines 449-610 (fill_from_rest per-symbol loop)
  </read_first>

<action>
1. At line ~449 (just before the `if let Some(from_id) = from_tid` block), add:
   ```rust
   let symbol_fill_start = std::time::Instant::now();
   let symbol_bars_before = total_bars;
   ```

1. Replace the existing completion log at line ~597 (`tracing::info!(%symbol, symbol_trades_total, ...)`) with a structured summary:

   ```rust
   tracing::info!(
       %symbol,
       duration_ms = symbol_fill_start.elapsed().as_millis() as u64,
       trade_count = symbol_trades_total,
       bars_produced = total_bars - symbol_bars_before,
       last_tid = current_from - 1,
       "fill_from_rest.symbol_summary"
   );
   ```

2. Also update the "no trailing trades" branch at line ~606 to include timing:

   ```rust
   tracing::info!(
       %symbol,
       duration_ms = symbol_fill_start.elapsed().as_millis() as u64,
       trade_count = 0u64,
       bars_produced = 0u64,
       "fill_from_rest.symbol_summary"
   );
   ```

</action>

<acceptance_criteria>

- grep: `fill_from_rest.symbol_summary` appears in `live_engine.rs`
- grep: `duration_ms` appears in the fill_from_rest symbol summary log line
- grep: `trade_count` appears in the fill_from_rest symbol summary log line
- grep: `bars_produced` appears in the fill_from_rest symbol summary log line
  </acceptance_criteria>

### Task 2: Junction telemetry — gap size, forming bar trades, WS buffer depth (TELE-01, TELE-02, TELE-03, AUDT-03, AUDT-05)

<read_first>

- `crates/opendeviationbar-streaming/src/live_engine.rs` lines 1284-1347 (junction fill block)
- `crates/opendeviationbar-streaming/src/live_engine.rs` lines 1238-1242 (trade_rx setup from pre_buffered_rx)
  </read_first>

<action>
1. Before the junction fill block (line ~1284), compute and log WS buffer depth. The `trade_rx` is an `mpsc::Receiver` — it does not expose `.len()`. Instead, log the buffer depth by draining with `try_recv` into a vec and re-sending, OR more practically, just log a note that WS buffer depth will be measured after first recv. **Better approach**: the `pre_buffered_rx` channel capacity is 1000 (line 319). We can't query pending count on tokio mpsc. Instead, add a counter: after receiving the first WS trade (line 1287), try_recv in a loop to measure buffer depth, then push all trades back. But mpsc is not re-pushable.

**Practical approach**: Add a `ws_buffer_depth` counter. After receiving the first trade at line 1287, drain all immediately-available trades into a `VecDeque`, count them as `ws_buffer_depth`, then process them after the junction fill (before the main loop). This gives exact buffer depth.

Replace lines 1287-1293 with:

```rust
// Drain all immediately-available WS trades to measure buffer depth
let first_ws_trade = match trade_rx.recv().await {
    Some(t) => t,
    None => {
        tracing::warn!(%symbol, "WS channel closed before first trade, exiting symbol_task");
        return;
    }
};
let mut ws_buffer: Vec<AggTrade> = Vec::new();
while let Ok(t) = trade_rx.try_recv() {
    ws_buffer.push(t);
}
let ws_buffer_depth = ws_buffer.len();
```

1. Enhance the existing junction info log at lines 1300-1306 to include all structured fields:

   ```rust
   // Compute forming bar trade count across all processors
   let forming_bar_trades = processors.first()
       .and_then(|(_, p)| p.get_incomplete_bar())
       .map(|b| b.trade_count)
       .unwrap_or(0);

   tracing::info!(
       %symbol,
       junction_gap_tid_delta = junction_gap,
       forming_bar_trades,
       ws_buffer_depth,
       rest_last_tid = rest_tid,
       ws_first_tid = first_ws_trade.agg_trade_id,
       "junction.telemetry"
   );
   ```

2. For the no-gap case (when `junction_gap <= 0` or `last_rest_tid` is None), still emit telemetry:

   ```rust
   // After the junction gap check block, before JUNC-03 re-seed:
   if last_rest_tid.is_none() || junction_gap <= 0 {
       let forming_bar_trades = processors.first()
           .and_then(|(_, p)| p.get_incomplete_bar())
           .map(|b| b.trade_count)
           .unwrap_or(0);
       tracing::info!(
           %symbol,
           junction_gap_tid_delta = 0i64,
           forming_bar_trades,
           ws_buffer_depth,
           "junction.telemetry"
       );
   }
   ```

3. After the junction fill and JUNC-04 processing of `first_ws_trade`, process the buffered WS trades before entering the main loop (so they are not lost):

   ```rust
   // Process buffered WS trades that arrived during junction fill
   for trade in &ws_buffer {
       if let Some((last_id, gap_size)) = gap_detector.check(trade.agg_trade_id) {
           let recovered = puck_fill(
               &symbol, last_id, trade.agg_trade_id,
               &mut processors, &bar_buffer, &metrics, &shutdown,
               rate_limiter.clone(), &forming_tx_map,
           ).await;
           if recovered > 0 {
               gap_detector.mark_filled(last_id, gap_size);
           }
       }
       process_trade_through_processors(
           trade, &mut processors, &mut last_days,
           &bar_buffer, &metrics, &symbol_arc, &symbol, &forming_tx_map,
       );
       gap_detector.update_last_tid(trade.agg_trade_id);
   }
   tracing::debug!(%symbol, drained = ws_buffer.len(), "junction: processed buffered WS trades");
   ```

</action>

<acceptance_criteria>

- grep: `junction.telemetry` appears in `live_engine.rs`
- grep: `junction_gap_tid_delta` appears in `live_engine.rs`
- grep: `forming_bar_trades` appears in `live_engine.rs`
- grep: `ws_buffer_depth` appears in `live_engine.rs`
- grep: `ws_buffer` variable declared in junction fill block
  </acceptance_criteria>

### Task 3: Puck event_type differentiation (TELE-04, AUDT-04)

<read_first>

- `crates/opendeviationbar-streaming/src/live_engine.rs` lines 1088-1104 (puck_fill function signature and log)
- `crates/opendeviationbar-streaming/src/live_engine.rs` lines 1295-1319 (junction puck_fill call)
- `crates/opendeviationbar-streaming/src/live_engine.rs` lines 1440-1466 (main loop puck_fill call)
  </read_first>

<action>
1. Add an `event_type` parameter to `puck_fill()`:
   ```rust
   async fn puck_fill(
       symbol: &str,
       last_known_id: i64,
       first_ws_id: i64,
       processors: &mut [(u32, OpenDeviationBarProcessor)],
       bar_buffer: &ConcurrentRingBuffer<CompletedBar>,
       metrics: &LiveEngineMetrics,
       shutdown: &CancellationToken,
       rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
       forming_tx_map: &HashMap<u32, watch::Sender<Option<FormingBar>>>,
       event_type: &str,  // "junction_fill" or "normal_gap_fill"
   ) -> u64 {
   ```

1. Update the tracing::info! at line 1101-1104 to include `event_type`:

   ```rust
   tracing::info!(
       %symbol, gap_size, last_known_id, first_ws_id, event_type,
       "puck: filling gap via parallel REST API"
   );
   ```

2. Update all 3 call sites:
   - Junction fill (line ~1307): pass `"junction_fill"`
   - Main loop gap detection (line ~1446): pass `"normal_gap_fill"`
   - On-demand gap-fill command (line ~1366): pass `"on_demand_gap_fill"`
     </action>

<acceptance_criteria>

- grep: `event_type` parameter in `puck_fill` function signature
- grep: `"junction_fill"` passed to puck_fill
- grep: `"normal_gap_fill"` passed to puck_fill
- grep: `"on_demand_gap_fill"` passed to puck_fill
- `cargo check -p opendeviationbar-streaming` succeeds (all call sites updated)
  </acceptance_criteria>

### Task 4: Verify build compiles

<read_first>

- (no additional reading needed)
  </read_first>

<action>
Run `cargo check -p opendeviationbar-streaming` to verify all changes compile.
Fix any compilation errors.
</action>

<acceptance_criteria>

- `cargo check -p opendeviationbar-streaming` exits with code 0
  </acceptance_criteria>
