GCP Log Schema
Structured logging schema for the optimization algorithm. All logs are emitted as JSON to stdout and parsed by Google Cloud Logging automatically.
This schema builds on the existing CloudLoggingFormatter in src/interceptor/logger.py, which already provides severity, timestamp, source location, and request correlation. The additions below standardise the service identification, step tracking, and step-specific payloads so that every log line from the optimizer can be filtered, queried, and dashboarded in Cloud Logging.
Core Envelope
Every log entry produced during an optimization run contains these fields:
| Field | Type | Source | Description |
|---|---|---|---|
severity |
string | Formatter | Cloud Logging severity: DEBUG, INFO, WARNING, ERROR, CRITICAL |
timestamp |
string (ISO 8601) | Formatter | UTC timestamp of the log entry |
message |
string | Formatter | Human-readable log message, prefixed with [step_id step_title] when a step context is active |
logger |
string | Formatter | Python logger name (record.name) |
optimization_id |
string | Formatter | Added at top level from the request_id context variable (application-generated identifier for this optimization run) |
service.type |
string | Formatter | Fixed: "optimizer" (from the SERVICE constant in logger.py) |
service.subtype |
string | Formatter | Fixed: "closer" |
logging.googleapis.com/sourceLocation.file |
string | Formatter | Source file name (e.g. clusterModel.py) |
logging.googleapis.com/sourceLocation.function |
string | Formatter | Function name (e.g. _convexCluster) |
logging.googleapis.com/sourceLocation.line |
int | Formatter | Line number in source file |
logging.googleapis.com/labels.request_id |
string | Formatter | Promoted from the request_id context variable |
logging.googleapis.com/labels.step_id |
string | Formatter | Promoted from the current step context (set by step_fields() or set_current_step()) |
exception |
string | Formatter | Formatted exception traceback — only present when logger.exception(...) is called |
step.step_id |
string | Call site (via step_fields) |
Algorithm step code (e.g. "2.3") |
step.step_title |
string | Call site (via step_fields) |
Algorithm step name (e.g. "Van Assignment") |
step.* |
varies | Call site (via step_fields) |
Step-specific payload fields (see per-step tables below) |
Example log entry (JSON)
{
"severity": "INFO",
"timestamp": "2026-07-06T10:23:45.123456+00:00",
"message": "[2.3 Van Assignment] The convex model was executed. solver_status=Optimal",
"logger": "optimizationModels.clusterModel.clusterModel",
"optimization_id": "20260706102344987654",
"service": {
"type": "optimizer",
"subtype": "closer"
},
"logging.googleapis.com/sourceLocation": {
"file": "clusterModel.py",
"function": "_convexCluster",
"line": 296
},
"logging.googleapis.com/labels": {
"request_id": "20260706102344987654",
"step_id": "2.3"
},
"step": {
"step_id": "2.3",
"step_title": "Van Assignment",
"solver_status": "Optimal"
}
}
Step Registry
Canonical step codes and titles. These values are used in the step.step_id and step.step_title fields.
step_id |
step_title |
Algorithm reference |
|---|---|---|
1.1 |
Order Classification & Fleet Sizing | ClusterModel.__init__ |
1.2 |
Bag Grouping | bags.compose_bags() |
2.1 |
Minimum Stop Enforcement | ClusterModel._func() |
2.2 |
Geographic Grouping | ClusterModel._clust2() |
2.3 |
Van Assignment | ClusterModel._convexCluster() |
2.4 |
Stop Redistribution | ClusterModel._min_satelite() |
2.5 |
Bike Attachment | ClusterModel._bikes() |
2.6 |
Capacity Balancing | ClusterModel._closer() |
2.7 |
Van Route Sequencing | ClusterModel._findSequenceVans() |
2.8 |
Delivery Target Check | ClusterModel._vans_cycle() |
3.1 |
Bike Route Sequencing | ClusterModel._findSequenceBikes() |
3.2 |
Bike Reassignment | ClusterModel._bikes_cycle() |
4 |
Box Pickup Routes | ClusterModel._composed_routes() |
5 |
Final Output | finalJson.createOutput() |
Cloud Logging Queries
The step object enables filtering by step code, step title, or step-specific payload fields:
-- All logs for a specific optimization run
jsonPayload.optimization_id = "20260706102344987654"
-- All logs from a specific step
jsonPayload.step.step_id = "2.7"
-- All logs from the Van Route Planning phase (steps 2.*)
jsonPayload.step.step_id =~ "^2\\."
-- All Delivery Target Check failures
jsonPayload.step.step_id = "2.8" AND jsonPayload.step.action = "add_van"
-- All unrouted van warnings
jsonPayload.step.step_id = "2.7" AND jsonPayload.step.n_unrouted > 0
-- SLA below target across all runs (2.8 summary carries the SLA)
jsonPayload.step.step_id = "2.8" AND jsonPayload.step.sla < 0.95
-- Convex solver infeasible / not optimal
jsonPayload.step.step_id = "2.3" AND jsonPayload.step.solver_status != "Optimal"
Cheatsheet — nice queries to run in Log Explorer
By request / optimization ID (set as both a label and a top-level field, so either works)
By algorithm step (label promoted from the step context; see registry above)
By step payload fields passed via step_fields(...) — anything in **payload lands under jsonPayload.step.*
jsonPayload.step.step_id = "2.3"
jsonPayload.step.solver_status != "Optimal"
jsonPayload.step.action = "add_van"
jsonPayload.step.n_unrouted > 0
jsonPayload.step.is_summary = true
jsonPayload.step.route4me_time_s > 30
By message / source
jsonPayload.message : "Route4me"
jsonPayload.message : "[2.7" -- messages are prefixed with [step_id step_title]
jsonPayload."logging.googleapis.com/sourceLocation".file = "clusterModel.py"
jsonPayload."logging.googleapis.com/sourceLocation".function = "_convexCluster"
jsonPayload.logger = "src.optimizationModels.clusterModel.clusterModel"
Combined — one optimization run, only the sequencing step's unrouted warnings
Implementation Rules
- All existing log lines are migrated to this schema. No new log lines are added — the current lines are converted in place.
- All severity levels (INFO, WARNING, ERROR) carry the full envelope including the
stepobject. A warning about an unrouted van in Step 2.7 still includesstep.step_id: "2.7"and all relevant payload fields.
Step-Specific Payloads
Each step adds its own fields inside the step object alongside step_id and step_title.
The tables below reflect the fields the code actually emits today (per step_fields(step_id, **payload) calls in src/optimizationModels/). Not every log line at a given step includes every field — most call sites only pass a subset. Fields are grouped by the call sites that emit them.
Step 1.1 — Order Classification & Fleet Sizing
Emitted from ClusterModel.__init__ (clusterModel.py:89, 103) at the start of each optimization run, across two log lines.
| Field | Type | Description | Where emitted |
|---|---|---|---|
n_orders |
int | Total number of delivery orders | first line |
n_van_orders |
int | Orders classified as van (normal) | first line |
n_bike_orders |
int | Orders classified as bike | first line |
n_bag_orders |
int | Orders classified as bag (composed) | first line |
n_slots |
int | Number of time slots in the shift | first line |
shift_start |
int | Shift start time (seconds from midnight) | first line |
shift_end |
int | Shift end time (seconds from midnight) | first line |
min_car |
int | Minimum number of vans calculated | second line |
n_vans_available |
int | Number of vans provided in the request | second line |
vehicle_capacity |
int | Effective van capacity (after PERCENTAGE_VEHICLE_CAPACITY) |
second line |
Step 1.2 — Bag Grouping
Declared in the step registry but not currently emitted — no step_fields("1.2") call site exists in the code today.
Step 2.1 — Minimum Stop Enforcement
Emitted from ClusterModel._func() (clusterModel.py:462, 473, 516, 523).
| Field | Type | Description |
|---|---|---|
n_missing |
int | Satellites still missing to hit the per-slot minimum |
n_promotions |
int | Bike orders promoted to van delivery on this call |
n_bikes_remaining |
int | Bike orders still classified as bike at the end of the step |
Step 2.2 — Geographic Grouping
Emitted from ClusterModel._clust2() (clusterModel.py:567), one log line per slot (not a single aggregated array).
| Field | Type | Description |
|---|---|---|
slot_time |
int | Slot start time (seconds from midnight) |
n_orders |
int | Orders in this slot |
n_clusters |
int | Clusters produced for this slot |
Step 2.3 — Van Assignment
Emitted from ClusterModel._convexCluster() / _bigClust() (clusterModel.py:192, 296, 609).
| Field | Type | Description |
|---|---|---|
solver_status |
string | PuLP solver status (e.g. "Optimal", "Infeasible") — only on the result line |
The other 2.3 lines (Variables started to be prepared…, Slots started to be aggregated…) carry no payload beyond step_id/step_title.
Step 2.4 — Stop Redistribution
Emitted from ClusterModel._min_satelite() (clusterModel.py:318, 361, 397, 416).
| Field | Type | Description |
|---|---|---|
n_moves |
int | Stops moved between clusters on this call (single-move lines emit 1) |
van_stop_counts |
array | Stops per van after redistribution — emitted only on the summary line |
Step 2.5 — Bike Attachment
Emitted from ClusterModel._bikes() (clusterModel.py:653, 669, 707, 710).
| Field | Type | Description |
|---|---|---|
n_bikes_attached |
int | Bike orders linked to a van stop on this call (only on the summary line) |
Other 2.5 lines carry no additional payload.
Step 2.6 — Capacity Balancing
Emitted from ClusterModel._closer() (clusterModel.py:730, 772, 800, 835, 860).
| Field | Type | Description |
|---|---|---|
van_id |
int | Van identifier — emitted on the "Van with overload found" line |
pieces |
int | Current load of that van (in pieces) — emitted alongside van_id |
capacity |
int | Van capacity (max_load) — emitted alongside van_id |
n_reallocated |
int | Deliveries moved from overloaded vans (only on the summary line) |
Step 2.7 — Van Route Sequencing
Emitted from ClusterModel._findSequenceVans() and _findSequenceVans2() (retry) (clusterModel.py:958, 969, 1012, 1028, 1045, 1049, 1093, 1160, 1174, 1191, 1195, 1225, 1267, 1278, 1284).
Per-van entry — one entry per Route4me call, plus separate UNROUTED warning entries when routes come back unrouted.
| Field | Type | Description |
|---|---|---|
van_id |
int | Van identifier |
cluster_id |
string | Cluster key |
n_addresses |
int | Addresses sent to Route4me (only on the "Calling Route4me" line) |
route4me_time_s |
float | Route4me API response time in seconds (only on the response line) |
n_routed |
int | Routed pieces (initial call only) |
n_unrouted |
int | Unrouted pieces / count of unrouted routes |
unrouted_order_ids |
array | Route4me order_ids of unrouted stops (empty [] if none) |
unrouted_dingoo_ids |
array | Dingoo order IDs for the same unrouted stops (paired warning line) |
route_id |
string | Emitted by _check_unrouted_vans() per unrouted route |
is_retry |
bool | false in the initial pass, true in _findSequenceVans2 |
Summary entry — one per batch (clusterModel.py:1093, 1225).
| Field | Type | Description |
|---|---|---|
is_summary |
bool | Always true |
n_vans_routed |
int | Number of van responses in the batch |
n_vans_total |
int | Total vans attempted (from setcars) |
total_routed |
int | Total fulfilled stops across all vans |
total_unrouted |
int | Total delayed stops across all vans |
sla |
float | Batch SLA (defaults to 0.0 if no stops) |
is_retry |
bool | true for the retry pass summary |
Step 2.8 — Delivery Target Check
Emitted from ClusterModel._vans_cycle() (clusterModel.py:1430, 1450, 1458, 1463, 1472, 1478, 1485, 1493, 1501, 1508).
| Field | Type | Description |
|---|---|---|
iteration |
int | Outer loop iteration (van-count increment) — only on cycle entry / final summary lines |
split_attempt |
int | Inner split loop attempt (0–2) — cycle entry line |
min_car |
int | Current minimum van count — cycle entry, "add_van" summary, final summary |
sla |
float | SLA after this attempt — most 2.8 lines |
sla_target |
float | Config.SLA_VANS_GOAL |
n_vans |
int | Number of van responses returned by _findSequenceVans[2] |
n_unrouted |
int | Unrouted vans detected before triggering retry |
action |
string | Next action: "pass" or "add_van" (no "split" value is emitted today) |
split_order_id |
string/null | Only emitted as null on the "No satellites to split" line |
Fields n_fulfilled and n_delayed are not emitted at step 2.8.
Step 3.1 — Bike Route Sequencing
Emitted from Bikes.calculate_sla(), Bikes.check_sla(), Bikes.create_json_bike() (bike.py:36, 50, 99, 110).
| Field | Type | Description |
|---|---|---|
parent |
int | Parent satellite order_id — only on the "Parent not found, falling back to depot" warning |
The other 3.1 lines (Calculating sla…, Checking sla…, Building bike request input…) carry no additional payload.
Fields n_bikes, n_routed, n_unrouted, unrouted_order_ids, route4me_time_s, is_retry are not emitted at step 3.1 today.
Step 3.2 — Bike Reassignment
Emitted from Bikes.check_sla() and ClusterModel._bikes_cycle() (bike.py:60, 72, 79, 92; clusterModel.py:1531, 1540, 1557, 1582).
| Field | Type | Description |
|---|---|---|
parent |
int | Parent satellite order_id (from Bikes reassignment lines) |
n_unrouted |
int | Number of unrouted bikes in the current cycle |
unrouted_order_ids |
array | Unrouted bike order_ids (only on the initial unrouted-count line) |
order_id |
int | The specific unrouted bike being processed (per-delivery lines) |
reason |
string | Only value emitted today: "no_valid_slot" |
Fields n_reassigned, n_to_depot, reassignments are not emitted.
Step 4 — Box Pickup Routes
Emitted from ClusterModel._composed_routes() (clusterModel.py:1602, 1619, 1663).
| Field | Type | Description |
|---|---|---|
n_boxes |
int | Total composed boxes in the input (only on the entry line) |
parent_id |
int/null | Parent order id for the current bag (per-bag line) |
route4me_time_s |
float | Route4me API response time in seconds (per-bag line) |
Field n_composed_routes is not emitted.
Step 5 — Final Output
Emitted from finalJson.createOutput() and _calcSla() (finalJson.py:16, 28, 40, 48, 73, 77, 100, 186, 190).
| Field | Type | Description |
|---|---|---|
unrouted_order_ids |
array | Unrouted order IDs for an unrouted route — only on the _calcSla "unrouted deliveries" line |
All other step-5 lines emit step_id/step_title only. Fields estimated_sla, n_fulfilled, n_delayed, n_unrouted, n_van_routes, n_bike_routes, total_pieces, elapsed_s are not currently attached to the log payload (the SLA numbers live in the response body under output["sla"] and output["stats"] when debug_stats=True).
Usage Pattern
The extra_fields mechanism in CloudLoggingFormatter merges custom fields into the JSON envelope. The service block, optimization_id, and both request_id/step_id labels are added by the formatter itself (from module constants and context variables) — call sites only need to attach the step object via the step_fields() helper.
Helper (as implemented in src/interceptor/logger.py)
# src/interceptor/logger.py
STEPS = {
"1.1": "Order Classification & Fleet Sizing",
"1.2": "Bag Grouping",
"2.1": "Minimum Stop Enforcement",
"2.2": "Geographic Grouping",
"2.3": "Van Assignment",
"2.4": "Stop Redistribution",
"2.5": "Bike Attachment",
"2.6": "Capacity Balancing",
"2.7": "Van Route Sequencing",
"2.8": "Delivery Target Check",
"3.1": "Bike Route Sequencing",
"3.2": "Bike Reassignment",
"4": "Box Pickup Routes",
"5": "Final Output",
}
def step_fields(step_id: str, **payload) -> dict:
"""Build extra dict with the step object for structured logging.
Also sets the current step context so downstream log lines
(e.g. from Route4me client) inherit the step prefix / label automatically.
"""
_current_step.set(step_id)
return {"extra_fields": {
"step": {
"step_id": step_id,
"step_title": STEPS[step_id],
**payload,
},
}}
Usage:
from interceptor.logger import step_fields
self.logger.info(
"Assigned %d clusters to %d vans",
n_clusters, n_vans,
extra=step_fields("2.3", solver_status=str(prob.status)),
)
Local development output
The LocalFormatter renders the step inline for readability: