Files
prautoblogger/includes/services/opik/class-opik-trace-context.php
peptiderepo fbbb387311 feat: Opik LLM observability tracing skeleton (Wave 1) — v0.12.0
Wires PRAutoblogger into Comet Opik cloud for per-call tracing.

- Thin PHP REST client (class-opik-client.php, 276 LOC)
- Async fire-and-forget dispatch via wp_schedule_single_event
- Feature flag PRAUTOBLOGGER_OPIK_ENABLED defaults OFF
- One trace per article, 5 spans across content-generator + chief-editor
- Exponential backoff 2s/4s/8s, max 3 retries, drops on failure
- Admin settings page (api-key configured indicator only, never rendered)
- ARCHITECTURE.md + CONVENTIONS.md updated

PR #113 — QA gate: PASS (qa-reviews/prautoblogger/2026-04-23-0d8dc67.md)
2026-04-23 22:36:24 +07:00

197 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
/**
* Per-request trace context singleton.
*
* Holds the current trace ID and manages the span stack for a single
* article generation. Initialized at the top of the pipeline,
* torn down at the end.
*
* Single-threaded per-request model: one trace per article generation,
* child spans per LLM call.
*
* @see includes/services/opik/class-opik-client.php
* @see includes/services/opik/class-opik-span-queue.php
*/
class PRAutoBlogger_Opik_Trace_Context {
/**
* @var self|null
*/
private static ?self $instance = null;
/**
* @var string Trace ID (UUID).
*/
private string $trace_id = '';
/**
* @var float Trace start time (microtime).
*/
private float $trace_start = 0.0;
/**
* @var array Stack of active spans; indexed by span_id.
*/
private array $span_stack = array();
/**
* Get or create the current request's trace context.
*
* @return self
*/
public static function current(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initialize a new trace for this request.
*
* Generates a UUID and records the start time. Called once at the
* top of the article generation pipeline.
*
* @return string The trace ID (UUID).
*/
public function init_trace(): string {
$this->trace_id = wp_generate_uuid4();
$this->trace_start = microtime( true );
return $this->trace_id;
}
/**
* Get the current trace ID.
*
* @return string
*/
public function get_trace_id(): string {
return $this->trace_id;
}
/**
* Start a span within the current trace.
*
* @param array{
* name: string,
* type: string,
* model?: string,
* provider?: string,
* input?: array,
* parent_span_id?: string
* } $span_data Span metadata.
*
* @return string Generated span ID (UUID).
*/
public function start_span( array $span_data ): string {
$span_id = wp_generate_uuid4();
$span = array(
'id' => $span_id,
'trace_id' => $this->trace_id,
'name' => $span_data['name'] ?? 'unknown',
'type' => $span_data['type'] ?? 'general',
'start_time' => gmdate( 'Y-m-d\TH:i:s\Z' ),
'start_time_ms' => microtime( true ),
'end_time' => null,
'end_time_ms' => null,
'input' => $span_data['input'] ?? array(),
'output' => array(),
'model' => $span_data['model'] ?? null,
'provider' => $span_data['provider'] ?? null,
'parent_span_id' => $span_data['parent_span_id'] ?? null,
'usage' => array(),
);
$this->span_stack[ $span_id ] = $span;
return $span_id;
}
/**
* End a span and record its output.
*
* @param string $span_id Span ID from start_span().
* @param array{
* output?: array,
* usage?: array
* } $span_data Final span data to merge.
*/
public function end_span( string $span_id, array $span_data = array() ): void {
if ( ! isset( $this->span_stack[ $span_id ] ) ) {
PRAutoBlogger_Logger::instance()->warning(
sprintf( 'Attempt to end unknown span %s', $span_id ),
'opik'
);
return;
}
$span = &$this->span_stack[ $span_id ];
$span['end_time'] = gmdate( 'Y-m-d\TH:i:s\Z' );
$span['end_time_ms'] = microtime( true );
$span['output'] = $span_data['output'] ?? array();
$span['usage'] = $span_data['usage'] ?? array();
}
/**
* Get a completed span from the stack.
*
* @param string $span_id
*
* @return array|null
*/
public function get_span( string $span_id ): ?array {
return $this->span_stack[ $span_id ] ?? null;
}
/**
* Get all spans collected in this trace.
*
* @return array
*/
public function get_spans(): array {
return $this->span_stack;
}
/**
* Finalize and return the trace object with all spans.
*
* @return array{
* id: string,
* name: string,
* project_name: string,
* start_time: string,
* end_time: string,
* input: array,
* output: array,
* metadata: array,
* spans: array
* }
*/
public function finalize_trace(): array {
$trace_end = microtime( true );
return array(
'id' => $this->trace_id,
'name' => 'article_generation',
'project_name' => get_option( 'prautoblogger_opik_project_name', 'prautoblogger' ),
'start_time' => gmdate( 'Y-m-d\TH:i:s\Z', (int) $this->trace_start ),
'end_time' => gmdate( 'Y-m-d\TH:i:s\Z', (int) $trace_end ),
'input' => array( 'pipeline' => 'article_generation' ),
'output' => array( 'spans_count' => count( $this->span_stack ) ),
'metadata' => array( 'source' => 'prautoblogger' ),
'spans' => $this->span_stack,
);
}
/**
* Tear down the context (clear it for the next request).
*
* Called at the end of the article generation pipeline.
*/
public static function teardown(): void {
self::$instance = null;
}
}