Files
prautoblogger/includes/services/opik/class-opik-span-queue.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

171 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* Async queue for Opik span/trace payloads.
*
* Backs the queue with WP options (a single serialized array stored
* in wp_options). Payloads are enqueued at the end of article
* generation, dequeued and batch-posted via cron.
*
* Max queue depth is 1000 items; older items are expired TTL-based.
*
* @see includes/services/opik/class-opik-dispatcher.php — Drains the queue.
*/
class PRAutoBlogger_Opik_Span_Queue {
/**
* @var string WP option key storing the queue.
*/
const QUEUE_OPTION_KEY = 'prautoblogger_opik_queue';
/**
* @var int Max queue depth (items).
*/
const MAX_QUEUE_DEPTH = 1000;
/**
* @var int Item TTL in seconds (12 hours).
*/
const ITEM_TTL_SECONDS = 43200;
/**
* Enqueue a span or trace for async dispatch.
*
* @param array $payload Span or trace payload.
* @param string $type Either 'span' or 'trace'.
*
* @return bool True if enqueued, false if queue full.
*/
public function enqueue( array $payload, string $type = 'span' ): bool {
if ( ! in_array( $type, array( 'span', 'trace' ), true ) ) {
$type = 'span';
}
$queue = $this->get_queue();
if ( count( $queue ) >= self::MAX_QUEUE_DEPTH ) {
PRAutoBlogger_Logger::instance()->warning(
sprintf( 'Opik queue full (%d items); dropping span', count( $queue ) ),
'opik'
);
return false;
}
$item = array(
'type' => $type,
'payload' => $payload,
'enqueued' => time(),
'attempt' => 0,
);
$queue[] = $item;
update_option( self::QUEUE_OPTION_KEY, $queue );
return true;
}
/**
* Dequeue the next N items from the queue for processing.
*
* @param int $limit Max items to dequeue.
*
* @return array Array of items with 'type', 'payload', 'enqueued', 'attempt'.
*/
public function dequeue( int $limit = 100 ): array {
$queue = $this->get_queue();
if ( empty( $queue ) ) {
return array();
}
// Remove expired items.
$now = time();
$queue = array_filter(
$queue,
function ( $item ) use ( $now ) {
return ( $now - $item['enqueued'] ) < self::ITEM_TTL_SECONDS;
}
);
// Grab the first $limit items.
$batch = array_splice( $queue, 0, $limit );
// Save the remaining queue.
update_option( self::QUEUE_OPTION_KEY, $queue );
return $batch;
}
/**
* Re-enqueue items that failed to post (typically after backoff).
*
* @param array $items Items from a failed batch.
*/
public function reenqueue( array $items ): void {
$queue = $this->get_queue();
foreach ( $items as $item ) {
$item['attempt'] = ( $item['attempt'] ?? 0 ) + 1;
// Drop items that have exceeded max retries (3).
if ( $item['attempt'] >= 3 ) {
PRAutoBlogger_Logger::instance()->warning(
sprintf(
'Dropping Opik item after %d attempts: %s',
$item['attempt'],
$item['type']
),
'opik'
);
continue;
}
$queue[] = $item;
}
update_option( self::QUEUE_OPTION_KEY, $queue );
}
/**
* Get the current queue state.
*
* @return array
*/
public function get_queue(): array {
$queue = get_option( self::QUEUE_OPTION_KEY, array() );
return is_array( $queue ) ? $queue : array();
}
/**
* Get queue depth (item count).
*
* @return int
*/
public function get_queue_depth(): int {
return count( $this->get_queue() );
}
/**
* Clear all items from the queue.
*
* @return void
*/
public function clear(): void {
delete_option( self::QUEUE_OPTION_KEY );
}
/**
* Get a human-readable status string for admin display.
*
* @return string
*/
public function get_status(): string {
$depth = $this->get_queue_depth();
if ( 0 === $depth ) {
return 'Queue empty';
}
return sprintf( 'Queue: %d item%s pending', $depth, 1 !== $depth ? 's' : '' );
}
}