Skip to content

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:

  1. kernel.request (after auth/rate-limiting):
  2. Only captures /api/v1/* requests
  3. Stores request body, method, path, IP in request attributes
  4. Determines direction and source from method + path pattern
  5. Extracts entityType from path, externalId from path or body

  6. kernel.response:

  7. Captures response body, status code, duration
  8. Persists the ApiLog entity

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:purge deletes 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