Skip to content

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)

labels.request_id = "<uuid>"
jsonPayload.optimization_id = "<uuid>"

By algorithm step (label promoted from the step context; see registry above)

labels.step_id = "2.3"                  -- exact step
labels.step_id =~ "^2\\."               -- all clustering / van steps

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

labels.request_id = "<uuid>"
labels.step_id = "2.7"
jsonPayload.step.n_unrouted > 0

Implementation Rules

  1. All existing log lines are migrated to this schema. No new log lines are added — the current lines are converted in place.
  2. All severity levels (INFO, WARNING, ERROR) carry the full envelope including the step object. A warning about an unrouted van in Step 2.7 still includes step.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:

2026-07-06 10:23:45 - [INFO] - [20260706102344987654] clusterModel - (clusterModel.py)._convexCluster(276) - [2.3 Van Assignment] Assigned 12 clusters to 4 vans in 2.3s