feat: comprehensive observability layer (diagnostics + metrics)

Aperta
#591 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@Kuldeep2822k ci sta già lavorando.

Dal 5/11/2026.

Valutazione

Questa issue non è ancora stata valutata.

Descrizione

cli core enhancement storage tests

Task: Comprehensive Observability Layer for PALEE CLI


Current State Analysis

PALEE currently has zero observability infrastructure:

  • No timing metrics anywhere in production code
  • No structured logging framework
  • No performance profiling or diagnostics
  • No historical records of command duration
  • No ability to reproduce performance regressions

This gap makes it impossible to answer basic questions like:

  • Why did palee adopt take 15 seconds on my 500-note vault?
  • Was the recent refactor faster or slower than before?
  • How many times was each phase of the scan called?
  • Where does time get spent during session start?

Architecture Context

graph TD
    A[User invokes palee] --> B{command?}
    B -->|adopt| C[src/cli/adopt.ts]
    B -->|session start| D[src/cli/session-start.ts]
    B -->|validate| E[src/cli/validate.ts]
    C --> F[vault scan via src/storage/vault-walker.ts]
    D --> G[load hot.md .palee/hot.md]
    E --> H[parse frontmatter src/storage/frontmatter.ts]
    F --> I[src/engine/sm2.ts SM-2 compute]
    G --> I
    H --> I
    I --> J[atomic writes src/storage/atomic-write.ts]
    J --> K[console.log output]
    
    style A fill:#8ba7d4,stroke:#222
    style K fill:#8bd4b2,stroke:#222

Current architecture shows single-path execution without any measurement hooks. Every layer executes directly to stdout, with no parallel tracking of durations, counts, or failures.


Proposed Solution Architecture

Multi-Layer Design
flowchart LR
    subgraph Commands["Application Commands"]
        A[adopt.ts]
        B[sessions/start.ts]
        C[validate.ts]
    end
    
    subgraph Observability["Observability Layer"]
        D[diagnostics.ts]
        E[recordEvent]
        F[writeSummary]
    end
    
    subgraph Storage["Storage Backends"]
        G[JSONL events.jsonl]
        H[metrics/session-id.json]
    end
    
    A --> D
    B --> D
    C --> D
    D --> E
    D --> F
    E --> G
    F --> H
    
    style D fill:#9c8ac9,stroke:#222,stroke-width:2px
    style E fill:#8ab8d4,stroke:#222,stroke-width:1px
    style G fill:#d48a8a,stroke:#222,stroke-width:1px

Key design principles:

  • Zero runtime dependencies: Uses Node.js built-in fs, performance.now()
  • Opt-in activation: Set PALEE_DIAGNOSTICS=1 environment variable
  • Non-blocking writes: Logging failures never corrupt main execution
  • Privacy-first: Sanitize apiKey, store under .palee/ directory only
  • Backward compatible: Existing behavior unchanged when disabled

Layer 1: Structured JSONL Event Streaming

Log Format Example

Each event is a single-line JSON object written atomically:

{"timestamp":"2026-11-04T22:45:00.123Z","session_id":"S-20261104T224500-a1b2c3d4","event_type":"vault_scan_start","payload":{"vault_path":"/home/user/study-notes","scope":"all"}}
{"timestamp":"2026-11-04T22:45:00.578Z","session_id":"S-20261104T224500-a1b2c3d4","event_type":"frontmatter_parse","payload":{"topics_parsed":128,"duration_ms":455,"warnings_oversized":2}}
{"timestamp":"2026-11-04T22:45:01.102Z","session_id":"S-20261104T224500-a1b2c3d4","event_type":"sm2_compute_end","payload":{"topics_processed":128,"duration_ms":524}}
{"timestamp":"2026-11-04T22:45:03.089Z","session_id":"S-20261104T224500-a1b2c3d4","event_type":"adopt_end","payload":{"duration_ms":2966,"adopted_count":47,"rejected_paths":[".git","node_modules"],"conflicts_occurred":0}}
Event Types to Instrument
Phase Event Type Payload Fields
Vault Scan vault_scan_start vault_path, scope, estimated_file_count
Vault Scan file_scanned file_path, size_bytes, is_markdown
Frontmatter frontmatter_parse topics_parsed, duration_ms, failed_count
Frontmatter validation_error file_path, error_type, message
Adoption adopt_start target_path, dry_run
Adoption adopt_success file_path, frontmatter_keys_written, duration_ms
Adoption adopt_conflict file_path, expected_fingerprint, actual_fingerprint
Validation validate_start vault_path, strict_mode
Validation validate_result status, total_topics, errors_count, warnings_count
Session session_start session_id, hot_memory_loaded, topic_count
Session session_end session_id, duration_ms, topics_updated
Error failure_event command, exit_code, error_type, stack_sanitized

Layer 2: Per-Run Metrics Summary

Compact JSON Summary at End of Run

Written automatically when diagnostic mode is enabled:

{
  "schema_version": 1,
  "session_id": "S-20261104T224500-a1b2c3d4",
  "command": "adopt",
  "start_iso": "2026-11-04T22:45:00.123Z",
  "end_iso": "2026-11-04T22:45:03.089Z",
  "total_duration_ms": 2966,
  "exit_code": 0,
  "phase_breakdown": {
    "vault_scan_time_ms": 455,
    "frontmatter_parse_time_ms": 524,
    "sm2_compute_time_ms": 892,
    "atomic_write_time_ms": 1095,
    "other_overhead_ms": 0
  },
  "counts": {
    "topics_scanned": 128,
    "topics_adapted": 47,
    "topics_rejected": 1,
    "skipped_dot_directories": 3,
    "oversized_notes_skipped": 2,
    "occ_conflicts_detected": 0
  },
  "warnings": [
    {"type": "oversized_note", "path": "big-topic.md", "size_bytes": 524288},
    {"type": "symlink_skipped", "path": "link-to-outside"}
  ],
  "memory_footprint_estimate_mb": 12.4,
  "config_used": {
    "vault_path": "/home/user/study-notes",
    "provider_configured": true,
    "json_output_enabled": false
  }
}
Per-Phase Timing Breakdown

For debugging slow runs, break down where milliseconds get consumed:

Command Critical Phases to Time
palee adopt vault scanning → frontmatter parsing → dependency graph build → atomic writes
palee session start hot.md reload → session history load → SM-2 queries → tool call count
palee validate scanner loop → per-rule evaluation → cycle detection → report generation
palee plan readiness gate → prerequisite resolution → mastery threshold filter
palee roadmap --from YAML parsing → link resolution → cycle detection → write validation

Comparison with Other CLI Tools

How Other CLI Tools Handle Observability
1. ESLint (JavaScript Linter)
  • Approach: --debug flag outputs detailed timing for each rule
  • Example output:
Processing... 
Rule 'no-unused-vars': 0.023s
Rule 'prefer-const': 0.012s
Time taken: 2.34s
2. Rust's cargo Build Tool
3. kubectl Kubernetes CLI
  • Approach: KUBECONFIG_DEBUG=1 logs HTTP request/response timestamps
  • Example:
$ KUBECONFIG_DEBUG=1 kubectl get pods
GET /api/v1/namespaces/default/pods - 23ms
GET /api/v1/namespaces/kube-system/pods - 15ms
4. Go's cobra CLI Framework
5. Git's Verbose Mode
  • Approach: -vvv flag shows low-level filesystem operations
  • Example:
git -vvv checkout branch
D:\cli-build\git-check-cache.c:23: read(0x7ff7...) = 4096
D:\cli-build\git-check-cache.c:25: lstat(".git/index") = 0
What We're NOT Doing

No OpenTelemetry instrumentation — too heavy for CLI, no central backend exists
No remote telemetry — violates INV-29 privacy boundary
No real-time dashboards — CLI runs once-and-exits, doesn't maintain state

These trade-offs align with the privacy-first and offline-first design philosophy.


User-Priority Scenarios

Scenario 1: "Why is session start slow after lunch?"

Without observability:

  • User reports "slow", maintainer asks "how slow?"
  • User responds "2 minutes"
  • Maintainer cannot prove whether hot.md rebuild vs. tool call vs. SM-2 compute caused delay

With observability:

{
  "session_start": {
    "hot_reload_time_ms": 12,
    "session_history_load_time_ms": 89,
    "sm2_queries_time_ms": 34,
    "tool_calls_count": 7,
    "total_latency_ms": 1247,
    "dominant_bottleneck": "tool_calls"
  }
}

Conclusion: 1.2s spent in tool calls; user can disable provider temporarily to confirm.


Scenario 2: Regression Detection After Refactor

Before PR lands:

{
  "command": "adopt",
  "avg_duration_ms": 2450,
  "stdev_ms": 123,
  "n_runs": 50
}

After PR lands:

{
  "command": "adopt",
  "avg_duration_ms": 3890,
  "stdev_ms": 412,
  "n_runs": 50
}

Diff script (palee metrics diff baseline.json post-pr.json) shows:

SLOWDOWN DETECTED: +58% (1440ms)
├─ vault_scan: -2ms (unchanged)
├─ frontmatter_parse: +523ms (+21%)
└─ sm2_compute: +919ms (+67%) ← PRIMARY culprit

Outcome: CI blocks merge until performance regression explained/fixed.


Implementation Plan

Phase 1: Foundation (~30 min)

Create src/cli/diagnostics.ts with three core functions:

import { writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';

interface DiagnosticEvent {
  timestamp: string;
  session_id: string;
  event_type: string;
  payload: Record<string, unknown>;
}

export function recordEvent(eventType: string, payload: Record<string, unknown>): void {
  const sessionId = getSessionId();
  const event: DiagnosticEvent = {
    timestamp: new Date().toISOString(),
    session_id: sessionId,
    event_type: eventType,
    payload: sanitizeSensitiveFields(payload),
  };
  
  const logPath = getLogPath();
  if (!existsSync(logPath)) {
    mkdirSync(join(logPath, '..'), { recursive: true });
  }
  
  appendFileSync(logPath, JSON.stringify(event) + '\n', 'utf8');
}

export async function writeSummary(command: string, metrics: Record<string, unknown>): Promise<void> {
  const sessionId = getSessionId();
  const summary = {
    schema_version: 1,
    session_id: sessionId,
    command,
    start_iso: /* captured earlier */,
    end_iso: new Date().toISOString(),
    total_duration_ms: /* measure at start */ ,
    ...metrics
  };
  
  const summaryPath = join(getMetricsPath(), `session-${sessionId}.json`);
  writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
}

function sanitizeSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {
  const sanitized = { ...obj };
  delete sanitized.apiKey;
  delete sanitized.api_key;
  delete sanitized.secret;
  return sanitized;
}

Phase 2: Command Instrumentation (~45 min)

Wrap key phases in adopt.ts:

import { recordEvent, writeSummary } from './diagnostics';

async function adoptionEntry(...): Promise<void> {
  const startTime = performance.now();
  const sessionId = generateSessionId();
  
  try {
    recordEvent('adopt_start', { scope: options.all ? 'all' : 'single', target_path });
    
    const scanStart = performance.now();
    const notes = walkVault(vaultDir);
    const scanDuration = performance.now() - scanStart;
    recordEvent('vault_scan_complete', { 
      topics_found: notes.length, 
      duration_ms: scanDuration 
    });
    
    const parseStart = performance.now();
    const parsed = notes.map(parseFrontmatter);
    const parseDuration = performance.now() - parseStart;
    recordEvent('frontmatter_parse_complete', { 
      topics_parsed: parsed.length, 
      duration_ms: parseDuration 
    });
    
    recordEvent('adopt_end', { 
      adopted_count, 
      rejected_paths,
      duration_ms: performance.now() - startTime 
    });
    
  } catch (e: unknown) {
    recordEvent('failure', { exitCode, errorType: e instanceof Error ? e.name : 'Unknown' });
    throw e;
  } finally {
    await writeSummary('adopt', {
      vault_scan_time_ms: scanDuration,
      frontmatter_parse_time_ms: parseDuration,
      // ... other metrics
    });
  }
}

Repeat pattern for session/start.ts and validate.ts.


Phase 3: Test Matrix (~60 min)
New test file: test/cli-diagnostics.test.ts
import { describe, test, expect, beforeEach, afterEach } from 'node:test';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { recordEvent, writeSummary, getLogPath } from '../src/cli/diagnostics';

describe('CLI Diagnostics Module', () => {
  let tempDir: string;
  
  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'palee-diag-test-'));
    process.env.PALEE_LOG_DIR = tempDir;
    process.env.PALEE_DIAGNOSTICS = '1';
  });
  
  afterEach(() => {
    rmSync(tempDir, { recursive: true, force: true });
    delete process.env.PALEE_LOG_DIR;
    delete process.env.PALEE_DIAGNOSTICS;
  });
  
  test('records events to JSONL file', () => {
    recordEvent('test_event', { field: 'value' });
    const lines = fs.readFileSync(getLogPath(), 'utf8').trim().split('\n');
    expect(lines).toHaveLength(1);
    const event = JSON.parse(lines[0]);
    expect(event.event_type).toBe('test_event');
    expect(event.payload.field).toBe('value');
  });
  
  test('sanitizes apiKey from payloads', () => {
    recordEvent('secret_log', { apiKey: 'should-be-hidden', visible: 'kept' });
    const lines = fs.readFileSync(getLogPath(), 'utf8').trim().split('\n');
    const event = JSON.parse(lines[0]);
    expect(event.payload).not.toHaveProperty('apiKey');
    expect(event.payload.visible).toBe('kept');
  });
  
  test('writes summary JSON at command end', async () => {
    await writeSummary('adopt', { duration_ms: 1234, topics_adapted: 5 });
    const summaryPath = join(getMetricsPath(), 'session-S-*.json');
    expect(existsSync(summaryPath)).toBe(true);
    const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
    expect(summary.command).toBe('adopt');
    expect(summary.duration_ms).toBe(1234);
  });
});

Update existing tests to verify timing capture:

  • test/cli-adopt-batch.test.ts: Assert vault_scan_time_ms logged
  • test/cli-session-start.test.ts: Assert tool_call_count included
  • test/storage-scanner.test.ts: Assert files_scanned counted
  • test/validation-report.test.ts: Assert violations_by_type grouped
  • test/cli-exit-codes.test.ts: Assert failure events have exit_code field

Phase 4: Documentation & ADR (~30 min)

Create ADR document: docs/adr/00XX-observability-layer.md

# ADR-00XX: Local Observability Layer for PALEE CLI

## Status
Accepted (2026-11-04)

## Context
PALEE lacks timing metrics and structured diagnostics. Users reporting "slow performance" have no way to collect runtime data without manual console.log intervention.

## Decision
Implement lightweight local observability with these constraints:

1. Zero third-party dependencies (uses Node.js built-in fs, performance.now())
2. Opt-in activation via PALEE_DIAGNOSTICS=1 environment variable
3. All data stored under .palee/ directory; no remote transmission
4. Non-blocking writes: logging failures never disrupt main execution flow
5. Privacy-preserving: apiKey and secrets filtered before any storage
6. Follows invariant INV-29 (network boundary) by avoiding network IO

## Data Formats

### JSONL Event Stream (.palee/logs/events.jsonl)
Append-only line-delimited JSON objects with fields: timestamp, session_id, event_type, payload.

### Per-Run Metrics (.palee/metrics/session-{id}.json)
Single JSON object per command invocation with fields: command, start_iso, end_iso, phase_breakdown, counts, warnings.

## Alternatives Considered

### Alternative 1: OpenTelemetry SDK
Pros: Standard format, portable exports  
Cons: Adds ~150KB bundle, requires learning curve, introduces dependency chain

Verdict: Deferred to future phases when centralized dashboard requirement emerges.

### Alternative 2: diagnostics_channel Module
Pros: Built into Node.js v22+, zero deps  
Cons: Experimental channel shapes, less developer control over export destinations

Verdict: Consider as follow-up optimization if OTel needed later.

### Alternative 3: Third-Party Logger (winston, pino)
Pros: Battle-tested, multi-backend support  
Cons: Runtime bloat, unnecessary complexity for CLI use case

Verdict: Reject — adds deps we don't need.

## Consequences

### Positive
- Debugging "slow adopt" becomes tractable (identify which phase dominates)
- Performance regressions detected pre-merge via CI baseline comparison
- User support improved with objective diagnostics data
- Test coverage expanded to include timing assertions

### Negative
- Minor file I/O overhead (~1-2ms per command)
- Extra disk space for .palee/logs/ and .palee/metrics/ directories (~5KB/day typical)
- Added maintenance burden for observers team

## References
- planning/palee_cli_spec.md - Section 6 (CLI Layer specs)
- planning/invariants.md - INV-29 (Network Boundary), INV-30 (JSON contract)
- src/cli/onboarding.ts - Line 68 (emitError function signature)
- ESLint timing docs: https://eslint.org/docs/latest/use/command-line-interface#show-time
- Cargo timings RFC: https://doc.rust-lang.org/cargo/reference/unstable.html#timings

Update README.md section:

Add new subsection under "Configuration & Environment Variables":

## Debugging & Diagnostics

Enable optional observability logging by setting:

```bash
export PALEE_DIAGNOSTICS=1

This creates two additional directories under .palee/:

  • .palee/logs/events.jsonl - Append-only stream of events (structured JSONL)
  • .palee/metrics/session-{id}.json - Compact summary per command run

Note: All observability data stays local. No network transmission occurs. Secrets like apiKey are automatically filtered before writing.

To view recent diagnostic events:

tail -n 50 ~/.palee/logs/events.jsonl

Or inspect latest session metrics:

cat ~/.palee/metrics/session-*.json | tail -20
Environment Variables
Variable Purpose Default
PALEE_CONFIG_DIR Override config directory path ~/.palee
PALEE_JSON Enable machine-readable output false
PALEE_DIAGNOSTICS Enable observability logging false
PALEE_LOG_DIR Override diagnostics log path ~/.palee/logs
PALEE_DEBUG Include stack traces on failure false

Acceptance Criteria Checklist

Phase 1: Core Implementation
  • src/cli/diagnostics.ts created with recordEvent() and writeSummary()
  • Session ID generation reuses existing .palee/sessions/ logic
  • Log path configuration respects PALEE_LOG_DIR env var
  • JSON sanitization removes apiKey and similar secrets
  • Unit tests pass: npm test test/cli-diagnostics.test.ts
Phase 2: Command Instrumentation
  • adopt.ts wraps 3 phases: vault scan, frontmatter parse, atomic writes
  • session/start.ts captures: hot reload time, session history load time
  • validate.ts logs: violation counts by type, scan duration
  • Performance overhead < 5ms per command (benchmark verified)
Phase 3: Test Coverage
  • 6+ existing tests updated to assert metrics captured
  • New test file test/cli-diagnostics.test.ts with 8+ test cases
  • Integration test verifies non-blocking behavior (logging failure doesn't crash)
  • Memory footprint increase < 1MB (verified with heap snapshot)
Phase 4: Documentation & Adoption
  • ADR document created at docs/adr/00XX-observability-layer.md
  • README.md updated with "Debugging & Diagnostics" subsection
  • All environment variables documented in CONTRIBUTING.md
  • Related invariants cited: INV-29, INV-30, INV-43
Phase 5: Optional Future Enhancement
  • palee diagnostics show command added (views last N events)
  • --last N flag controls number of events shown (default 10)
  • --json flag dumps raw JSONL (human mode shows summary)

Scope Boundaries

In Scope (Phase 1)
  • Duration tracking for core commands
  • Structured JSONL event logging (opt-in)
  • Per-run metrics summary files
  • Error failure recording with sanitized stacks
  • Full test coverage matrix
  • Zero-runtime-dependency implementation
Out of Scope (Phase 1)
  • Remote telemetry / cloud backend
  • Real-time dashboard or alerting
  • Distributed tracing across machines
  • Performance regression alerts (these come later)
  • Auto-generating performance recommendations

Related Issues

  • #166 - Fix diagnostics: non-finite frontmatter values render as null in validation findings
    (Shows existing concern for diagnostic accuracy; this extends the pattern to timing/metrics)

  • #565 - Confirmation prompts hang on EOF
    (Session continuity contract; observability will help track checkpoint counts and retry attempts)

  • ADR-0024 - Exit Codes and Error Contract
    (Preserves existing exit code semantics while adding structured metadata)


Labels

  • enhancement - Feature request
  • core - Core subsystem impact
  • tests - Requires new test suite
  • cli - CLI layer primary component

Estimated Effort

Component Time Dependencies
src/cli/diagnostics.ts implementation ~30 min None
Command instrumentation (3 commands) ~45 min diagnostics.ts complete
Test matrix + verification ~60 min diagnostics.ts + instrumentation
Documentation + ADR creation ~30 min Final implementation locked
Total ~2.5 hours Sequential progression

Risk Assessment: Low risk — purely additive changes, no breaking modifications to existing APIs, optional opt-in activation preserves backward compatibility.


Author

Created through comprehensive analysis of codebase, community web search, and architecture review. Follows repository template format for consistency with existing GitHub workflows.

Lingua principale
TypeScript
Stelle
1
Fork
0
Merge medio
15h 5m
PR unite (30g)
163

Preparare l'ambiente

  • Nessun Dockerfile né file Docker Compose
  • Ha un modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Kuldeep2822k/cli

Tutte le issue di Kuldeep2822k/cli

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.