PluginProbe
AI / trunk
AI vtrunk
1.3.0 1.2.0 1.1.0 1.0.2 1.0.1 1.0.0 0.9.0 trunk 0.1.1 0.2.0 0.2.1 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0
ai / includes / Logging / REST / AI_Request_Log_Controller.php

AI_Request_Log_Controller.php in AI trunk, at includes/Logging/REST/AI_Request_Log_Controller.php

331 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API controller for AI request logs.
4 *
5 * @package WordPress\AI\Logging
6 */
7
8 declare( strict_types=1 );
9
10 namespace WordPress\AI\Logging\REST;
11
12 use WP_Error;
13 use WP_REST_Controller;
14 use WP_REST_Request;
15 use WP_REST_Response;
16 use WP_REST_Server;
17 use WordPress\AI\Logging\AI_Request_Log_Manager;
18
19 defined( 'ABSPATH' ) || exit;
20
21 /**
22 * Provides `/ai/v1/logs` routes for the AI Request Logs admin UI.
23 *
24 * @since 1.0.0
25 */
26 class AI_Request_Log_Controller extends WP_REST_Controller {
27
28 /**
29 * Log manager instance.
30 */
31 private AI_Request_Log_Manager $manager;
32
33 /**
34 * Constructor.
35 *
36 * @param \WordPress\AI\Logging\AI_Request_Log_Manager $manager Log manager.
37 */
38 public function __construct( AI_Request_Log_Manager $manager ) {
39 $this->namespace = 'ai/v1';
40 $this->rest_base = 'logs';
41 $this->manager = $manager;
42 }
43
44 /**
45 * Registers REST routes.
46 */
47 public function register_routes(): void {
48 // GET /ai/v1/logs - List logs with filtering.
49 // DELETE /ai/v1/logs - Purge all logs.
50 register_rest_route(
51 $this->namespace,
52 '/' . $this->rest_base,
53 array(
54 array(
55 'methods' => WP_REST_Server::READABLE,
56 'callback' => array( $this, 'get_logs' ),
57 'permission_callback' => array( $this, 'permissions_check' ),
58 'args' => $this->get_collection_params(),
59 ),
60 array(
61 'methods' => WP_REST_Server::DELETABLE,
62 'callback' => array( $this, 'purge_logs' ),
63 'permission_callback' => array( $this, 'permissions_check' ),
64 ),
65 )
66 );
67
68 // GET /ai/v1/logs/summary - Get aggregate statistics.
69 register_rest_route(
70 $this->namespace,
71 '/' . $this->rest_base . '/summary',
72 array(
73 array(
74 'methods' => WP_REST_Server::READABLE,
75 'callback' => array( $this, 'get_summary' ),
76 'permission_callback' => array( $this, 'permissions_check' ),
77 'args' => array(
78 'period' => array(
79 'type' => 'string',
80 'enum' => array( 'minute', 'hour', 'day', 'week', 'month', 'all' ),
81 'default' => 'day',
82 ),
83 ),
84 ),
85 )
86 );
87
88 // GET /ai/v1/logs/filters - Get filter options.
89 register_rest_route(
90 $this->namespace,
91 '/' . $this->rest_base . '/filters',
92 array(
93 array(
94 'methods' => WP_REST_Server::READABLE,
95 'callback' => array( $this, 'get_filters' ),
96 'permission_callback' => array( $this, 'permissions_check' ),
97 ),
98 )
99 );
100
101 // GET /ai/v1/logs/{id} - Get single log entry.
102 register_rest_route(
103 $this->namespace,
104 '/' . $this->rest_base . '/(?P<id>[a-f0-9\-]+)',
105 array(
106 array(
107 'methods' => WP_REST_Server::READABLE,
108 'callback' => array( $this, 'get_log' ),
109 'permission_callback' => array( $this, 'permissions_check' ),
110 'args' => array(
111 'id' => array(
112 'type' => 'string',
113 'required' => true,
114 'validate_callback' => static function ( $value ): bool {
115 return wp_is_uuid( $value );
116 },
117 ),
118 ),
119 ),
120 )
121 );
122 }
123
124 /**
125 * Permission check - restricted to administrators.
126 */
127 public function permissions_check(): bool {
128 return current_user_can( 'manage_options' );
129 }
130
131 /**
132 * Retrieves logs with filtering and pagination.
133 *
134 * @param \WP_REST_Request $request Request.
135 * @return \WP_REST_Response
136 */
137 public function get_logs( WP_REST_Request $request ): WP_REST_Response {
138 $args = array(
139 'type' => $request->get_param( 'type' ) ?? '',
140 'status' => $request->get_param( 'status' ) ?? '',
141 'provider' => $request->get_param( 'provider' ) ?? '',
142 'operation' => $request->get_param( 'operation' ) ?? '',
143 'tokens_filter' => $request->get_param( 'tokens_filter' ) ?? '',
144 'user_id' => $request->get_param( 'user_id' ) ?? 0,
145 'date_from' => $request->get_param( 'date_from' ) ?? '',
146 'date_to' => $request->get_param( 'date_to' ) ?? '',
147 'search' => $request->get_param( 'search' ) ?? '',
148 'page' => $request->get_param( 'page' ) ?? 1,
149 'per_page' => $request->get_param( 'per_page' ) ?? 25,
150 'orderby' => $request->get_param( 'orderby' ) ?? 'timestamp',
151 'order' => $request->get_param( 'order' ) ?? 'DESC',
152 'cursor_id' => $request->get_param( 'cursor_id' ),
153 'cursor_timestamp' => $request->get_param( 'cursor_timestamp' ),
154 );
155
156 $result = $this->manager->get_logs( $args );
157
158 $response = rest_ensure_response( $result['items'] );
159 $response->header( 'X-WP-Total', (string) $result['total'] );
160 $response->header( 'X-WP-TotalPages', (string) $result['pages'] );
161
162 // Include cursor info for cursor-based pagination.
163 if ( isset( $result['next_cursor'] ) ) {
164 $response->header( 'X-WP-NextCursorId', (string) $result['next_cursor']['id'] );
165 $response->header( 'X-WP-NextCursorTimestamp', $result['next_cursor']['timestamp'] );
166 }
167
168 return $response;
169 }
170
171 /**
172 * Retrieves a single log entry.
173 *
174 * @param \WP_REST_Request $request Request.
175 * @return \WP_REST_Response|\WP_Error
176 */
177 public function get_log( WP_REST_Request $request ) {
178 $log_id = $request->get_param( 'id' );
179 $log = $this->manager->get_log( $log_id );
180
181 if ( ! $log ) {
182 return new WP_Error(
183 'wpai_log_not_found',
184 __( 'Log entry not found.', 'ai' ),
185 array( 'status' => 404 )
186 );
187 }
188
189 return rest_ensure_response( $log );
190 }
191
192 /**
193 * Retrieves aggregate statistics.
194 *
195 * @param \WP_REST_Request $request Request.
196 * @return \WP_REST_Response
197 */
198 public function get_summary( WP_REST_Request $request ): WP_REST_Response {
199 $period = $request->get_param( 'period' ) ?? 'day';
200 $summary = $this->manager->get_summary( $period );
201
202 return rest_ensure_response( $summary );
203 }
204
205 /**
206 * Retrieves filter options.
207 *
208 * @param \WP_REST_Request $request Request.
209 * @return \WP_REST_Response
210 */
211 public function get_filters( WP_REST_Request $request ): WP_REST_Response {
212 $filters = $this->manager->get_filter_options();
213
214 return rest_ensure_response( $filters );
215 }
216
217 /**
218 * Purges all logs.
219 *
220 * @param \WP_REST_Request $request Request.
221 * @return \WP_REST_Response
222 */
223 public function purge_logs( WP_REST_Request $request ): WP_REST_Response {
224 $deleted = $this->manager->purge_all_logs();
225
226 return rest_ensure_response(
227 array(
228 'success' => true,
229 'deleted' => $deleted,
230 'message' => sprintf(
231 /* translators: %d: Number of deleted logs. */
232 __( 'Successfully purged %d log entries.', 'ai' ),
233 $deleted
234 ),
235 )
236 );
237 }
238
239 /**
240 * Gets collection parameters for logs list endpoint.
241 *
242 * @return array<string, array<string, mixed>> Parameter definitions.
243 */
244 public function get_collection_params(): array {
245 return array(
246 'type' => array(
247 'description' => __( 'Filter by log type.', 'ai' ),
248 'type' => 'string',
249 'enum' => array_merge( array( '' ), AI_Request_Log_Manager::get_types() ),
250 'default' => '',
251 ),
252 'status' => array(
253 'description' => __( 'Filter by status.', 'ai' ),
254 'type' => 'string',
255 'enum' => array( '', 'success', 'error', 'timeout' ),
256 'default' => '',
257 ),
258 'provider' => array(
259 'description' => __( 'Filter by AI provider.', 'ai' ),
260 'type' => 'string',
261 'default' => '',
262 ),
263 'operation' => array(
264 'description' => __( 'Filter by operation. Accepts a single value or a comma-separated list.', 'ai' ),
265 'type' => 'string',
266 'default' => '',
267 ),
268 'user_id' => array(
269 'description' => __( 'Filter by user ID.', 'ai' ),
270 'type' => 'integer',
271 'default' => 0,
272 ),
273 'date_from' => array(
274 'description' => __( 'Filter logs from this date (YYYY-MM-DD HH:MM:SS).', 'ai' ),
275 'type' => 'string',
276 'format' => 'date-time',
277 ),
278 'date_to' => array(
279 'description' => __( 'Filter logs until this date (YYYY-MM-DD HH:MM:SS).', 'ai' ),
280 'type' => 'string',
281 'format' => 'date-time',
282 ),
283 'search' => array(
284 'description' => __( 'Search in operations, request previews, response previews, and error messages.', 'ai' ),
285 'type' => 'string',
286 'default' => '',
287 ),
288 'tokens_filter' => array(
289 'description' => __( 'Filter by tokens: "gt:N", "lt:N", or "none".', 'ai' ),
290 'type' => 'string',
291 'default' => '',
292 ),
293 'page' => array(
294 'description' => __( 'Current page of the collection.', 'ai' ),
295 'type' => 'integer',
296 'default' => 1,
297 'minimum' => 1,
298 ),
299 'per_page' => array(
300 'description' => __( 'Maximum number of items per page.', 'ai' ),
301 'type' => 'integer',
302 'default' => 25,
303 'minimum' => 1,
304 'maximum' => 100,
305 ),
306 'orderby' => array(
307 'description' => __( 'Sort collection by attribute.', 'ai' ),
308 'type' => 'string',
309 'enum' => array( 'timestamp', 'type', 'operation', 'duration_ms', 'tokens_total', 'status' ),
310 'default' => 'timestamp',
311 ),
312 'order' => array(
313 'description' => __( 'Order sort attribute ascending or descending.', 'ai' ),
314 'type' => 'string',
315 'enum' => array( 'ASC', 'DESC' ),
316 'default' => 'DESC',
317 ),
318 'cursor_id' => array(
319 'description' => __( 'Cursor ID for cursor-based pagination (use with cursor_timestamp).', 'ai' ),
320 'type' => 'integer',
321 'minimum' => 1,
322 ),
323 'cursor_timestamp' => array(
324 'description' => __( 'Cursor timestamp for cursor-based pagination (use with cursor_id).', 'ai' ),
325 'type' => 'string',
326 'format' => 'date-time',
327 ),
328 );
329 }
330 }
331