textmachine/backend/internal/store/requestlog.go

132 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package store
import (
"context"
"log/slog"
"textmachine/backend/internal/obs"
)
// RequestLog is one telemetry row (Р7: маршрут, токены с cache-полями, $,
// latency per книга/глава/чанк/стадия/роль/модель). Telemetry is strictly off
// the money path: деньги живут в spend/checkpoints, а сбой записи телеметрии
// логируется и никогда не роняет перевод.
type RequestLog struct {
TraceID string
BookID string
Chapter int
ChunkIdx int
Stage string
Role string
ModelRequested string
ModelActual string
RequestHash string
PromptTokens int
CachedTokens int
CacheCreationTokens int
CompletionTokens int
ReasoningTokens int
CostUSD float64
LatencyMS int
FinishReason string
TMHit bool // served from a checkpoint, no call was made
Degraded string
Err string
OK bool
}
// InsertRequestLog writes one telemetry row synchronously (a CLI pipeline has
// no answer-latency to protect, unlike vojo's chat path; the async+recover
// pattern returns if this ever sits on a hot path). Errors are the caller's to
// log as WARN — never to fail the pipeline on.
func (s *Store) InsertRequestLog(rl RequestLog) error {
ctx, cancel := opContext()
defer cancel()
_, err := s.w.ExecContext(ctx, `
INSERT INTO request_log (
trace_id, book_id, chapter, chunk_idx, stage, role,
model_requested, model_actual, request_hash,
prompt_tokens, cached_tokens, cache_creation_tokens,
completion_tokens, reasoning_tokens,
cost_usd, latency_ms, finish_reason, tm_hit, degraded, err, ok
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
rl.TraceID, rl.BookID, rl.Chapter, rl.ChunkIdx, rl.Stage, rl.Role,
rl.ModelRequested, rl.ModelActual, rl.RequestHash,
rl.PromptTokens, rl.CachedTokens, rl.CacheCreationTokens,
rl.CompletionTokens, rl.ReasoningTokens,
rl.CostUSD, rl.LatencyMS, rl.FinishReason, rl.TMHit, rl.Degraded, rl.Err, rl.OK)
return err
}
// LogRequest is the fire-and-forget wrapper the runner uses: insert, WARN on
// failure, never propagate.
func (s *Store) LogRequest(ctx context.Context, log *slog.Logger, rl RequestLog) {
if rl.TraceID == "" {
rl.TraceID = obs.TraceFromContext(ctx)
}
if err := s.InsertRequestLog(rl); err != nil && log != nil {
log.WarnContext(ctx, "request_log insert failed (non-fatal)", "err", err)
}
}
// FreshCallLatencyMS sums the wall-clock latency of the book's FRESH provider calls (tm_hit=0
// — a checkpoint replay has no answer latency to count) and their count. It is the throughput
// input for the `tmctl status` ETA (D15.3): mean fresh-call latency × remaining work, a
// throughput estimate, never a synthetic time-bar. Pure read, $0.
func (s *Store) FreshCallLatencyMS(bookID string) (totalMS int64, calls int, err error) {
ctx, cancel := opContext()
defer cancel()
err = s.r.QueryRowContext(ctx,
`SELECT COALESCE(SUM(latency_ms),0), COUNT(*) FROM request_log WHERE book_id = ? AND tm_hit = 0`,
bookID).Scan(&totalMS, &calls)
return
}
// RequestLogView is one request_log row for inspection (tmctl report).
type RequestLogView struct {
TS string
Stage string
Role string
ModelActual string
PromptTokens int
CachedTokens int
CacheCreationTokens int
CompletionTokens int
ReasoningTokens int
CostUSD float64
LatencyMS int
FinishReason string
TMHit int
OK int
}
// RequestLogRows returns all request_log rows for a book (tmctl report / приёмка
// Фазы 0). Строки МАТЕРИАЛИЗУЮТСЯ под op-таймаутом и возвращаются срезом —
// отдавать *sql.Rows нельзя: ленивая итерация у вызывающего переживает
// `defer cancel()` этого метода, и контекст отменяется ПОСРЕДИ чтения
// («context canceled», обрыв таблицы report — находка реальной приёмки).
func (s *Store) RequestLogRows(bookID string) ([]RequestLogView, error) {
ctx, cancel := opContext()
defer cancel()
rows, err := s.r.QueryContext(ctx, `
SELECT ts, stage, role, model_actual, prompt_tokens, cached_tokens,
cache_creation_tokens, completion_tokens, reasoning_tokens,
cost_usd, latency_ms, finish_reason, tm_hit, ok
FROM request_log WHERE book_id = ? ORDER BY id`, bookID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []RequestLogView
for rows.Next() {
var v RequestLogView
if err := rows.Scan(&v.TS, &v.Stage, &v.Role, &v.ModelActual,
&v.PromptTokens, &v.CachedTokens, &v.CacheCreationTokens,
&v.CompletionTokens, &v.ReasoningTokens, &v.CostUSD,
&v.LatencyMS, &v.FinishReason, &v.TMHit, &v.OK); err != nil {
return nil, err
}
out = append(out, v)
}
return out, rows.Err()
}