API Log: Input/Output Visibility
Date: 2026-02-16 Status: Approved
Problem
The middleware sits between Odoo and the webshop, but there is no way to see raw input/output. You can view entity state (currentData, previousData, changedFields) but not what Odoo actually sent or what the webshop received.
Solution
A single ApiLog entity captures all /api/v1/* HTTP traffic. A Symfony kernel event subscriber automatically intercepts every API request and response.
Data Model
ApiLog Entity
| Field | Type | Purpose |
|---|---|---|
id |
int (auto) | Primary key |
method |
string(10) | HTTP method: GET, POST, DELETE |
path |
string(500) | Request path |
direction |
enum | inbound (Odoo→MW) or outbound (MW→Webshop) |
source |
string(20) | odoo or webshop |
entityType |
string(50), nullable | product, partner, sale_order, stock, etc. |
externalId |
string(255), nullable | Entity's external ID (when identifiable) |
requestBody |
text, nullable | Raw JSON request body |
responseBody |
text, nullable | Raw JSON response body |
statusCode |
smallint | HTTP response status code |
ipAddress |
string(45) | Client IP |
duration |
int, nullable | Request duration in milliseconds |
createdAt |
datetime | Timestamp |
Indexes: (entityType, externalId), (createdAt), (direction).
Direction Logic
| Request | Direction | Source |
|---|---|---|
POST /api/v1/products |
inbound | odoo |
POST /api/v1/partners |
inbound | odoo |
POST /api/v1/sale-orders |
inbound | odoo |
POST /api/v1/stocks |
inbound | odoo |
GET /api/v1/updated-products |
outbound | webshop |
GET /api/v1/updated-partners |
outbound | webshop |
GET /api/v1/updated-sale-orders |
outbound | webshop |
GET /api/v1/stocks (GET) |
outbound | webshop |
GET /api/v1/pricelists/*/items |
outbound | webshop |
GET /api/v1/purchase-orders |
outbound | webshop |
POST /api/v1/failures |
inbound | webshop |
POST /api/v1/updates/.../ack |
inbound | webshop |
POST /api/v1/outbound-queue/... |
outbound | odoo |
Capture Mechanism
ApiLogSubscriber - Kernel event subscriber:
kernel.request(after auth/rate-limiting):- Only captures
/api/v1/*requests - Stores request body, method, path, IP in request attributes
- Determines direction and source from method + path pattern
-
Extracts entityType from path, externalId from path or body
-
kernel.response: - Captures response body, status code, duration
- Persists the
ApiLogentity
Entity type extraction:
- Path-based: /api/v1/products -> product
- ExternalId from path: /api/v1/updated-products/123 -> 123
- ExternalId from body: POST payloads contain external_id
- Batch requests: logged once with null externalId
Skipped: Non-API routes (admin, login, /.well-known/ai-context).
Admin UI
1. Dedicated API Log Page (/admin/api-logs)
- Sidebar item in admin navigation
- Index: Filterable table - Time, Method, Path, Direction, Source, Entity Type, Status Code, Duration
- Filters: Direction, source, entity type, status code range, date range
- Show: Full detail with pretty-printed JSON for request/response bodies
2. Entity Timeline Tab
On product/partner/sale-order detail pages, a section showing API log entries for that entity:
- Filtered by entityType + externalId
- Chronological list: timestamp, direction arrow, method, status code, expandable body
Example for Product PROD-001:
14:03 <- POST /api/v1/products (Odoo sent update) - 200
14:05 -> GET /api/v1/updated-products/PROD-001 (Webshop fetched) - 200
14:06 <- POST /api/v1/updates/product/PROD-001/ack (Acknowledged) - 200
Retention
- Console command
app:api-log:purgedeletes entries older than 30 days - Run via cron (daily)
Decisions
- Single table - no separate timeline entity; timeline derived by filtering
- Kernel subscriber - automatic, no per-controller instrumentation needed
- Batch requests - logged once with null externalId
- 30-day retention - balances visibility and storage