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)
This commit is contained in:
196
includes/services/opik/class-opik-trace-context.php
Normal file
196
includes/services/opik/class-opik-trace-context.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user