wpmind/modules/api-gateway/includes/Pipeline/LogMiddleware.php
LinuxJoy adb6c49301 style: phpcbf 自动格式化 — 全量 WPCS 3.x 规范对齐
135 文件 19,211 处自动修复:空格→Tab 缩进、括号间距、
函数声明空格、前置自增、尾逗号等纯格式化改动。
零逻辑变更,php -l + token 级对比验证通过。

新增 phpcs.xml.dist / phpstan.neon.dist 项目配置。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:48:46 +08:00

249 lines
6.8 KiB
PHP

<?php
/**
* Log Middleware
*
* Pipeline stage that writes audit log entries and updates
* API key usage counters for every gateway request.
*
* @package WPMind\Modules\ApiGateway\Pipeline
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\ApiGateway\Pipeline;
/**
* Class LogMiddleware
*
* Always executes (finally semantics). Writes to the audit log
* table and increments usage counters. Failures are silently
* logged -- logging must never break the API response.
*/
final class LogMiddleware implements GatewayStageInterface {
/**
* {@inheritDoc}
*/
public function process( GatewayRequestContext $context ): void {
try {
$this->write_audit_log( $context );
$this->update_key_usage( $context );
} catch ( \Throwable $e ) {
// Logging must never cause the request to fail.
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log(
sprintf(
'[WPMind API Gateway] Log middleware error for request %s: %s',
$context->request_id(),
$e->getMessage()
)
);
}
}
/**
* Write an entry to the audit log table.
*
* @param GatewayRequestContext $context Request context.
*/
private function write_audit_log( GatewayRequestContext $context ): void {
global $wpdb;
$has_error = $context->has_error();
$event_type = $has_error ? 'api_error' : 'api_request';
// Build detail JSON.
$detail = $this->build_detail( $context );
// Determine actor_user_id (0 for anonymous).
$user_id = get_current_user_id();
$actor_user_id = $user_id > 0 ? $user_id : 0;
// Privacy-preserving IP hash.
$ip_hash = $this->hash_client_ip( $context );
// User-Agent, truncated to 255 chars.
$user_agent = $context->rest_request()->get_header( 'user-agent' );
if ( is_string( $user_agent ) && mb_strlen( $user_agent ) > 255 ) {
$user_agent = mb_substr( $user_agent, 0, 255 );
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$inserted = $wpdb->insert(
$wpdb->prefix . 'wpmind_api_audit_log',
[
'event_type' => $event_type,
'key_id' => $context->key_id(),
'actor_user_id' => $actor_user_id,
'request_id' => $context->request_id(),
'ip_hash' => $ip_hash,
'user_agent' => $user_agent,
'detail_json' => wp_json_encode( $detail ),
'created_at' => current_time( 'mysql', true ),
],
[
'%s', // event_type
'%s', // key_id
'%d', // actor_user_id
'%s', // request_id
'%s', // ip_hash
'%s', // user_agent
'%s', // detail_json
'%s', // created_at
]
);
if ( $inserted === false ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log(
sprintf(
'[WPMind API Gateway] Failed to insert audit log for request %s: %s',
$context->request_id(),
$wpdb->last_error
)
);
}
}
/**
* Update the API key usage counters (atomic upsert).
*
* Only runs for successful requests with a valid key_id.
*
* @param GatewayRequestContext $context Request context.
*/
private function update_key_usage( GatewayRequestContext $context ): void {
// Only for successful requests with a key.
if ( $context->has_error() ) {
return;
}
$key_id = $context->key_id();
if ( $key_id === null ) {
return;
}
global $wpdb;
$table = $wpdb->prefix . 'wpmind_api_key_usage';
$window_month = gmdate( 'Y-m' );
$now = current_time( 'mysql', true );
// Extract token counts from internal result if available.
$result = $context->get_internal_result();
$input_tokens = 0;
$output_tokens = 0;
$total_tokens = 0;
$cost_usd = 0.0;
if ( is_array( $result ) ) {
$usage = $result['usage'] ?? [];
if ( is_array( $usage ) ) {
$input_tokens = (int) ( $usage['prompt_tokens'] ?? 0 );
$output_tokens = (int) ( $usage['completion_tokens'] ?? 0 );
$total_tokens = (int) ( $usage['total_tokens'] ?? 0 );
}
$cost_usd = (float) ( $result['cost_usd'] ?? 0.0 );
}
// Atomic upsert: INSERT ... ON DUPLICATE KEY UPDATE.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$query_result = $wpdb->query(
$wpdb->prepare(
'INSERT INTO %i
( key_id, window_month, request_count, input_tokens, output_tokens, total_tokens, total_cost_usd, updated_at )
VALUES ( %s, %s, 1, %d, %d, %d, %f, %s )
ON DUPLICATE KEY UPDATE
request_count = request_count + 1,
input_tokens = input_tokens + VALUES(input_tokens),
output_tokens = output_tokens + VALUES(output_tokens),
total_tokens = total_tokens + VALUES(total_tokens),
total_cost_usd = total_cost_usd + VALUES(total_cost_usd),
updated_at = VALUES(updated_at)',
$table,
$key_id,
$window_month,
$input_tokens,
$output_tokens,
$total_tokens,
$cost_usd,
$now
)
);
if ( $query_result === false ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log(
sprintf(
'[WPMind API Gateway] Failed to update key usage for %s: %s',
$key_id,
$wpdb->last_error
)
);
}
}
/**
* Build the detail JSON object for the audit log entry.
*
* @param GatewayRequestContext $context Request context.
* @return array<string, mixed>
*/
private function build_detail( GatewayRequestContext $context ): array {
$detail = [
'operation' => $context->operation(),
'status' => $context->has_error() ? $this->get_error_status( $context ) : 200,
'elapsed_ms' => $context->elapsed_ms(),
];
// Include error code if present.
if ( $context->has_error() ) {
$detail['error_code'] = $context->error()->get_error_code();
}
// Include model and provider from internal payload if available.
$payload = $context->get_internal_payload();
if ( is_array( $payload ) ) {
if ( isset( $payload['model'] ) ) {
$detail['model'] = $payload['model'];
}
if ( isset( $payload['provider'] ) ) {
$detail['provider'] = $payload['provider'];
}
}
return $detail;
}
/**
* Extract the HTTP status code from a WP_Error.
*
* @param GatewayRequestContext $context Request context.
* @return int HTTP status code.
*/
private function get_error_status( GatewayRequestContext $context ): int {
$error = $context->error();
$data = $error->get_error_data();
if ( is_array( $data ) && isset( $data['status'] ) ) {
return (int) $data['status'];
}
return 500;
}
/**
* Generate an HMAC-SHA256 hash of the client IP for privacy.
*
* Uses the IP resolved by AuthMiddleware via the context.
*
* @param GatewayRequestContext $context Request context.
* @return string 64-character hex hash.
*/
private function hash_client_ip( GatewayRequestContext $context ): string {
$ip = $context->client_ip() ?? '127.0.0.1';
return hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) );
}
}