1. Perché vi servono i log strutturati in ChatGPT App
Immaginate che vi scriva il product manager: «Gli utenti si lamentano che, quando scelgono un regalo, a volte appare una lista vuota e a volte il checkout va in crash. Possiamo sistemarlo per la demo di domani?». Avete:
- ChatGPT, che a volte chiama la vostra App — e a volte no.
- Un widget in sandbox.
- Un server MCP che interroga un catalogo prodotti esterno e l’ACP.
- Webhook dal fornitore di pagamenti.
E solo log testuali sparsi tipo «something went wrong» da qualche parte nel MCP e «order failed» da qualche parte nel backend. Con richieste in parallelo diventa il caos: impossibile capire quale log appartenga a quale utente e a quale richiesta.
I log JSON strutturati e un trace_id unico servono proprio a:
- vedere con un solo identificatore l’intera catena: dalla richiesta di ChatGPT al webhook "order.created";
- filtrare i log per servizio, tool, utente, scenario;
- rispondere rapidamente alle domande «perché il checkout è fallito» e «cosa stava facendo l’agente prima di iniziare a allucinare».
L’obiettivo è semplice: fare in modo che GiftGenius in produzione si possa fare debug e monitorare non peggio di una normale applicazione a microservizi.
2. Log testuali vs log strutturati: perché console.log("ops") non basta più
Nello sviluppo Next.js molti si accontentano di log testuali: stampano una frase leggibile e magari un paio di valori. In un servizio singolo è ancora accettabile. Ma nello stack di una ChatGPT App questi log diventano presto una poltiglia.
Un log testuale è solo una riga in un file o in console. Per esempio:
console.error(`Error in suggestGifts for user ${userId}: ${error.message}`);
Quando i messaggi sono centomila, trovare «tutti gli errori MCP nel checkout con userId=… di ieri» non è semplice. E costruire automaticamente una dashboard degli errori dei tool — quasi impossibile.
Un log strutturato è un oggetto JSON in cui, oltre al testo del messaggio, c’è un set di campi: livello, tempo, servizio, identificatori, contesto tecnico e di business. L’analogo del precedente:
logger.error({
message: "suggest_gifts failed",
user_id: userId,
trace_id,
service: "mcp",
tool_name: "suggest_gifts",
error_message: error.message,
});
Ogni campo viene indicizzato dal sistema di logging (ELK, Loki, Better Stack, Datadog, ecc.) e poi si possono eseguire query tipo service="mcp" AND level="error" AND tool_name="suggest_gifts" oppure cercare semplicemente per trace_id="...".
Per chiarezza — una piccola tabella.
| Cosa confrontiamo | Log testuali | Log strutturati (JSON) |
|---|---|---|
| Parsing | Manuale, tramite regex | Automatico per campi |
| Ricerca per campi | Query regexp complesse | Espressioni semplici field=value |
| Aggregazioni e dashboard | Difficile, molti workaround | Banale: count() , group by field |
| Arricchimento con contesto | Testo nel messaggio | Nuovi campi senza cambiare lo schema |
| Correlazione delle richieste | Quasi impossibile con richieste parallele | Ricerca normale per trace_id/request_id |
Nel mondo delle applicazioni LLM, dove metà dei problemi non è «errore 500», ma «il modello ha chiamato il tool sbagliato», senza log strutturati siete letteralmente ciechi.
3. Anatomia di un log JSON per ChatGPT App
Concordiamo un «mini‑standard» di record di log che userete in tutti i layer di GiftGenius. Non è perfetto, ma copre l’80% dei casi.
Spezziamo i campi dei log in alcuni gruppi.
Campi tecnici
Servono per far capire agli strumenti di osservabilità da dove arrivi il record.
Possiamo descriverli con un tipo TypeScript:
type LogLevel = "debug" | "info" | "warn" | "error";
interface BaseLogFields {
timestamp: string; // ISO 8601 UTC
level: LogLevel; // "info", "error"...
service: string; // "app-widget", "mcp", "agent", "commerce", "webhook"
env: "dev" | "staging" | "prod";
message: string; // Descrizione breve dell'evento
}
È meglio scrivere timestamp in formato ISO UTC ("2025-11-21T10:15:30.123Z"), così i vari servizi possono essere ordinati per tempo senza problemi di fusi orari. service ed env aiutano a separare, ad esempio, i log dell’MCP in produzione da quelli del widget in dev. È particolarmente utile se in futuro vorrete usare OpenTelemetry e le convenzioni comuni come service.name, service.version, ecc.
Campi di correlazione
Sono i più importanti per questa lezione. Senza di loro non riuscirete a collegare gli eventi tra loro.
Aggiungiamoli alla nostra interfaccia:
interface CorrelationFields {
trace_id: string; // ID end-to-end dell'intero scenario
span_id?: string; // (opzionale) ID dell'operazione specifica
parent_span_id?: string; // (opzionale) Operazione padre
request_id?: string; // ID locale della richiesta HTTP o della chiamata al tool
agent_run_id?: string; // ID dell'esecuzione dell'agente (se presente)
tool_call_id?: string; // ID della chiamata a uno specifico tool
checkout_session_id?: string; // ID della sessione ACP/pagamento
}
trace_id è il protagonista. Deve essere lo stesso in tutti i log che appartengono allo scenario «L’utente chiede di trovare un regalo, noi lo troviamo, creiamo l’ordine, riceviamo il webhook». span_id e parent_span_id permettono di costruire in seguito un «albero di operazioni» in stile distributed tracing, ma per iniziare si può vivere anche solo con trace_id e request_id.
Contesto di business
Un log tecnico senza contesto di business diventa «è successo qualcosa, da qualche parte, in qualche momento». Dobbiamo capire quale utente e in quale fase dello scenario è stato coinvolto.
Estendiamo l’interfaccia:
interface BusinessFields {
user_id?: string; // ID anonimo, NON l'email
tenant_id?: string; // Organizzazione/account, se B2B
flow?: string; // Ad esempio, "gift_recommendation" o "checkout"
step?: string; // Ad esempio, "collect_requirements" o "create_checkout"
}
Il principio è molto semplice: gli identificatori possono essere interni (UUID dal vostro DB), ma non devono contenere PII (email, telefono, nome e cognome). Ne parleremo ancora nella sezione sulla sicurezza.
Campi di errore
Gli errori meritano un discorso a parte. Un log tipico d’errore conviene suddividerlo almeno in tipo, codice e testo:
interface ErrorFields {
error_type?: "validation" | "upstream" | "timeout" | "system";
error_code?: string; // Stato HTTP, codice DB o un proprio enum
error_message?: string; // Breve e sicuro
stack?: string; // Stack, attenzione a volume e PII
}
È importante che error_message non contenga dati sensibili (tipo «failed for card 4111 1111 1111 1111»). Meglio "payment provider declined card" e un codice sicuro.
Interfaccia completa del log
Mettiamo tutto insieme:
export interface LogEvent
extends BaseLogFields,
CorrelationFields,
BusinessFields,
ErrorFields {
// lasciamo spazio per campi aggiuntivi
[key: string]: unknown;
}
Questa interfaccia potete usarla nel server MCP, nel backend commerce e nell’agente. Così tutti i servizi scriveranno log nello stesso formato, e la correlazione diventerà una passeggiata piacevole invece di una caccia al tesoro.
4. Logger JSON minimalista per GiftGenius (server MCP)
Partiamo da qualcosa di molto minimale. Supponiamo che il vostro server MCP sia un’app Node.js/TypeScript. Creiamo l’utility logger:
// mcp/logging.ts
import { LogEvent, LogLevel } from "./types";
function log(level: LogLevel, event: Omit<LogEvent, "level" | "timestamp">) {
const enriched: LogEvent = {
timestamp: new Date().toISOString(),
level,
env: process.env.NODE_ENV === "production" ? "prod" : "dev",
...event,
};
// Stampiamo il JSON su stdout — poi lo raccoglierà il sistema di log
console.log(JSON.stringify(enriched));
}
export const logger = {
debug: (event: Omit<LogEvent, "level" | "timestamp">) =>
log("debug", event),
info: (event: Omit<LogEvent, "level" | "timestamp">) =>
log("info", event),
warn: (event: Omit<LogEvent, "level" | "timestamp">) =>
log("warn", event),
error: (event: Omit<LogEvent, "level" | "timestamp">) =>
log("error", event),
};
Non è Pino né Winston, ma per il corso ci interessa l’idea: tutto viene scritto come JSON con campi sensati.
Ora usiamolo nell’handler del tool MCP suggest_gifts.
5. Logging dello strumento MCP: dall'ingresso all'uscita
Supponiamo di avere già un handler del tool suggest_gifts, che riceve le preferenze dell’utente e restituisce una lista di SKU. Aggiungiamoci i log.
Diciamo che abbiamo già estratto trace_id dall’header HTTP x-trace-id (come mettercelo — lo vedremo nel prossimo blocco sulla correlazione).
// mcp/tools/suggestGifts.ts
import { logger } from "../logging";
export async function suggestGiftsTool(args: SuggestGiftsArgs, ctx: {
traceId: string;
userId?: string;
}) {
logger.info({
message: "suggest_gifts called",
service: "mcp",
trace_id: ctx.traceId,
user_id: ctx.userId,
tool_name: "suggest_gifts",
flow: "gift_recommendation",
step: "fetch_candidates",
});
try {
const gifts = await fetchGiftsFromCatalog(args);
logger.info({
message: "suggest_gifts succeeded",
service: "mcp",
trace_id: ctx.traceId,
user_id: ctx.userId,
tool_name: "suggest_gifts",
flow: "gift_recommendation",
step: "rank_candidates",
result_count: gifts.length,
});
return gifts;
} catch (error: any) {
logger.error({
message: "suggest_gifts failed",
service: "mcp",
trace_id: ctx.traceId,
user_id: ctx.userId,
tool_name: "suggest_gifts",
flow: "gift_recommendation",
step: "fetch_candidates",
error_type: "upstream",
error_message: error.message,
});
throw error;
}
}
Ora, con un solo trace_id, potrete vedere:
- che il tool è stato effettivamente chiamato;
- quanti candidati sono stati trovati;
- a quale passo è andato in errore.
E in nessun punto compare l’email o il nome dell’utente — solo il user_id interno.
6. Dove nasce trace_id in ChatGPT App
Vediamo dove dovrebbe nascere il trace_id. È importante capire che non è legato a una singola richiesta. trace_id è l’identificatore di un’operazione di business. Quindi distinguiamo due situazioni tipiche:
«Stretto» strumento MCP
È quando il tool esegue un’operazione compatta e restituisce subito il risultato (senza UI interattiva):
- get_gifts_for_budget
- calculate_price
- save_lead, ecc.
In questo caso è comodo considerare: una chiamata al tool MCP = una richiesta di business = una traccia. Il trace_id end‑to‑end nasce sul lato MCP‑gateway / server MCP all’ingresso della tool‑call (o viene preso da un contesto di tracing esistente, se usate OpenTelemetry). Poi questo trace_id viene usato in tutte le chiamate interne (servizi REST, DB, code) e finisce nei log come campo trace_id.
ChatGPT e l’Apps SDK qui non intervengono: inviano solo la tool‑call JSON‑RPC, e il tracing inizia da voi, nella zona sotto il vostro controllo.
«Ampio» strumento MCP (restituisce un widget)
Qui il tool non completa l’operazione di business fino in fondo, ma avvia una scena interattiva: restituisce un widget che in sandbox esegue decine di fetch() (caricamento della lista di regali, filtri, checkout, ecc.).
In questo scenario il tracing end‑to‑end è diverso:
- le principali operazioni di business vivono nelle richieste HTTP del widget verso il backend;
- quindi ogni fetch() significativo dal widget al vostro backend ottiene il proprio trace_id, che nasce già nel backend / gateway (il primo hop server per quel fetch).
Né ChatGPT né il widget sono «fonte di verità» per il trace_id: possono solo passare nella richiesta alcuni identificatori ausiliari (session_id, widget_id, user_id), mentre la creazione e la gestione del trace_id avvengono sul server.
«Stretto» MCP‑tool: una traccia per tool‑call
Vediamo il flusso per un tool «stretto» senza widget:
sequenceDiagram
participant ChatGPT as ChatGPT / Agent
participant MCP as MCP Server
participant GiftAPI as Gift API
participant Pricing as Pricing API
ChatGPT->>MCP: JSON-RPC tools.call get_gifts
MCP->>MCP: start trace (trace_id = T-123)
MCP->>GiftAPI: GET /gifts (x-trace-id = T-123)
GiftAPI-->>MCP: 200 OK (trace_id = T-123)
MCP->>Pricing: GET /price (x-trace-id = T-123)
Pricing-->>MCP: 200 OK (trace_id = T-123)
MCP-->>ChatGPT: tool result (opzionalmente con trace_id)
Pattern:
- all’ingresso della tool‑call nel MCP create una traccia (o ne prendete una esistente da traceparent/x-trace-id);
- tutto il percorso successivo di quella tool‑call (chiamate ai servizi, DB, cache) viene loggato con lo stesso trace_id;
- nei log non compare il widget, perché il widget non c’è.
Questo approccio offre:
- uno «snapshot» chiaro di un’operazione: «MCP‑tool suggest_gifts → Gift API → Pricing API → risposta»;
- un trace_id per una chiamata al tool.
«Ampio» MCP‑tool: widget e più trace
Ora lo scenario GiftGenius, in cui lo strumento MCP restituisce un widget:
- ChatGPT chiama il tool MCP, per esempio open_gift_widget.
- Il tool MCP forma la descrizione del widget (layout, stato iniziale) e la restituisce.
- Il widget viene montato in sandbox e inizia a vivere di vita propria:
- GET /api/gifts?budget=50&page=1
- GET /api/gifts?budget=50&filter=for_developers
- POST /api/checkout
- POST /api/save-lead
- Ogni richiesta HTTP arriva al vostro backend Next.js / gateway — e lì create una nuova traccia:
fetch #1 -> trace_id = T-501 (caricare la prima pagina dei regali)
fetch #2 -> trace_id = T-502 (applicare il filtro «per sviluppatori»)
fetch #3 -> trace_id = T-503 (creare il checkout)
...
Quindi:
- il tool MCP è «ampio»: il suo compito principale è aprire il widget, non eseguire l’intera catena di business;
- la logica di business reale (lista dei regali, scelta del regalo top, checkout) vive nel backend, che gestisce i fetch() del widget;
- il gruppo di richieste fetch() riunite da un unico scenario di business ha il proprio trace_id, che generate sul server all’ingresso della richiesta HTTP.
In più potete far transitare in ogni traccia:
- session_id (ID di sessione ChatGPT, se presente),
- widget_id,
- user_id,
- tool_run_id o qualunque altro contesto.
Con trace_id guardate l’operazione specifica («checkout #3»), con session_id / widget_id — tutto ciò che è successo nell’ambito di uno stesso widget/sessione.
7. Correlazione delle richieste: come trace_id attraversa App, MCP, widget e backend
Passiamo alla parte più interessante: come fare in modo che gli identificatori necessari attraversino tutti i layer: ChatGPT, server MCP, widget, backend commerce e webhook.
Flusso delle richieste con trace_id (diagramma del caso «ampio»)
Piccolo schema di come appare per GiftGenius:
sequenceDiagram
participant ChatGPT as ChatGPT UI
participant MCP as MCP Server
participant Widget as GiftGenius Widget
participant Backend as Next.js Backend
participant ACP as Commerce API
participant WH as Webhook Handler
ChatGPT->>MCP: tools.call open_gift_widget
MCP-->>ChatGPT: Widget description (layout, config)
ChatGPT->>Widget: Render del widget nella sandbox
Widget->>Backend: GET /api/gifts (trace_id = T-501, generato nel Backend)
Backend->>ACP: GET /gifts (x-trace-id = T-501)
ACP-->>Backend: 200 OK (trace_id = T-501)
Backend-->>Widget: JSON con i regali (trace_id = T-501 nei log)
Widget->>Backend: POST /api/checkout (trace_id = T-503, generato nel Backend)
Backend->>ACP: POST /checkout (x-trace-id = T-503)
ACP-->>Backend: 200 OK (trace_id = T-503)
ACP-->>WH: webhook order.created (x-trace-id = T-503)
WH->>WH: Logga l'evento (trace_id = T-503)
Nota bene:
- in questo schema il trace_id non viene generato dal widget;
- compare nel punto di ingresso della richiesta HTTP nel vostro backend (Next.js route handler, API gateway, ecc.);
- poi questo trace_id viene propagato:
- nei log del backend,
- nell’header x-trace-id quando chiamate l’ACP,
- nei webhook, se l’ACP lo restituisce/propaga a valle.
6.5. Generare e propagare trace_id nel backend per le chiamate dal widget
Riscriviamo l’esempio in modo che sia chiaro: il trace_id nasce nel backend, non nel widget.
// app/api/mcp/tools/call/route.ts (Next.js backend, proxy verso MCP)
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { logger } from "@/mcp/logging";
export async function POST(req: NextRequest) {
// Se arriva un trace_id dal mondo esterno (per es., dal gateway) — usiamolo.
// Altrimenti ne generiamo uno nuovo all’ingresso nel backend.
const incomingTraceId = req.headers.get("x-trace-id");
const traceId = incomingTraceId ?? uuidv4();
const requestId = uuidv4();
logger.info({
message: "mcp.tools.call received from widget",
service: "backend",
trace_id: traceId,
request_id: requestId,
});
const body = await req.json();
const res = await fetch(process.env.MCP_SERVER_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-trace-id": traceId,
},
body: JSON.stringify(body),
});
const json = await res.json();
logger.info({
message: "mcp.tools.call completed",
service: "backend",
trace_id: traceId,
request_id: requestId,
});
return NextResponse.json(json);
}
Dal lato server MCP leggeremo semplicemente questo header e useremo il trace_id nei nostri log (come negli esempi della sezione 5).
Il widget può anche non sapere dell’esistenza del trace_id — gli basta chiamare /api/mcp/tools/call. Ma se vi è comodo visualizzare o loggare le azioni della UI legandole al tracing, potete restituire il trace_id nella risposta e scrivere, per esempio, service: "app-widget" nei vostri log JSON (lato client o via analytics SaaS).
Esempio di chiamata dal widget al MCP
// app/lib/mcpClient.ts (widget)
export async function callMcpTool(toolName: string, args: unknown) {
const res = await fetch("/api/mcp/tools/call", {
method: "POST",
headers: {
"Content-Type": "application/json",
// trace_id NON viene generato qui — nascerà nel backend
},
body: JSON.stringify({ toolName, args }),
});
// Se il backend restituisce trace_id nel body, si può salvarlo:
const data = await res.json();
return data;
}
Se volete, potete estendere l’handler del backend per aggiungere il trace_id nella risposta JSON, e allora il widget potrà:
- loggare eventi del tipo "service": "app-widget", "trace_id": "...",
- mostrare link alla traccia per gli sviluppatori.
Ma il principio resta lo stesso: la fonte del trace_id è il server, non il widget.
Propagare il trace_id verso ACP/commerce
Ora, all’interno del tool MCP create_checkout_session, chiamiamo il vostro commerce API e continuiamo a portare il trace_id negli header:
// mcp/tools/createCheckout.ts
import { logger } from "../logging";
export async function createCheckoutTool(
args: CreateCheckoutArgs,
ctx: { traceId: string; userId?: string }
) {
logger.info({
message: "create_checkout called",
service: "mcp",
trace_id: ctx.traceId,
user_id: ctx.userId,
tool_name: "create_checkout_session",
flow: "checkout",
step: "create_session",
});
const res = await fetch(process.env.COMMERCE_URL + "/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-trace-id": ctx.traceId,
},
body: JSON.stringify({
userId: ctx.userId,
...args,
}),
});
if (!res.ok) {
logger.error({
message: "checkout API failed",
service: "mcp",
trace_id: ctx.traceId,
user_id: ctx.userId,
flow: "checkout",
step: "create_session",
error_type: "upstream",
error_code: String(res.status),
});
throw new Error("Checkout API failed");
}
const data = await res.json();
logger.info({
message: "checkout session created",
service: "mcp",
trace_id: ctx.traceId,
user_id: ctx.userId,
flow: "checkout",
step: "create_session",
checkout_session_id: data.sessionId,
});
return data;
}
A sua volta, il backend commerce leggerà x-trace-id e lo scriverà nei propri log JSON. Così, con un solo trace_id, vedrete:
- la richiesta HTTP in ingresso dal widget al backend (dove è nata la traccia);
- il proxy verso il MCP (se c’è);
- la chiamata interna create_checkout_session;
- la richiesta al commerce API;
- la risposta del backend commerce;
- e, se lo propaga, il webhook order.created.
8. Livelli di log: DEBUG, INFO, WARN, ERROR nel contesto di un’app LLM
I livelli di log aiutano a non affogare nelle informazioni. Nella ChatGPT App è utile interpretarli così:
- DEBUG — informazioni tecniche dettagliate, utili in dev/staging. Per esempio, prompt abbreviati, stati intermedi dell’agente, risposte «raw» degli API esterni (senza PII). In produzione vanno trattati con molta cautela.
- INFO — eventi di business normali: «suggest_gifts riuscito, 10 candidati», «checkout session created», «webhook order.created processed». Questi log possono restare attivi in produzione.
- WARN — qualcosa è andato in modo non standard, ma il sistema ha continuato a funzionare. Per esempio: «fallback al catalogo in cache per timeout upstream», «il modello ha restituito argomenti del tool non validi, retry con schema diverso».
- ERROR — fallimento evidente: lo scenario non si è concluso come previsto. Per esempio: «checkout API failed», «failed to persist order», «tool crashed with unhandled exception».
Per comodità si può aggiungere un helper semplice, così da non scrivere le stringhe a mano:
type LogLevel = "debug" | "info" | "warn" | "error";
function isProd() {
return process.env.NODE_ENV === "production";
}
export function shouldLogLevel(level: LogLevel): boolean {
if (isProd()) {
return level === "info" || level === "warn" || level === "error";
}
return true; // in dev si registra tutto
}
E chiamare logger.debug solo quando shouldLogLevel("debug") restituisce true.
È particolarmente rischioso in produzione scrivere log DEBUG con il prompt completo e la risposta del modello: è facile che contengano password, chiavi, o qualunque PII che l’utente abbia incollato per errore nella chat.
9. Sicurezza dei log: PII‑scrub e segreti
Con i log è facile esagerare. Se scrivete «tutto e subito», rischiate di:
- violare le leggi sulla protezione dei dati;
- facilitare la vita a un attaccante (segret e token si possono estrarre dai log);
- temere voi stessi a chi dare accesso al sistema di logging.
Quindi vale un principio semplice: nei log c’è abbastanza informazione per capire cosa è successo, ma non abbastanza per rubare dati.
Buone pratiche:
- Registriamo user_id, non email o telefono. Se vi serve davvero l’email per il debug, loggatene l’hash o mascheratela ("a***@gmail.com").
- Non scriviamo mai nei log token completi ("sk-..."), refresh token, client_secret, password. Se proprio serve — solo le prime/ultime 4 cifre e il tipo («sk-***1234»).
- Attenzione a tool_input e tool_output. Possono contenere qualsiasi cosa abbia scritto l’utente. In produzione o non loggateli per intero, oppure:
- loggate solo i campi tipizzati, già validati;
- tagliate a dimensioni ragionevoli e applicate lo scrub — mascheramento con regex (email, numeri di carta, ecc.).
Esempio semplicissimo di sanitizzatore (molto semplificato):
export function sanitize(text: string): string {
return text
.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***redacted***")
.replace(/\b\d{16}\b/g, "****-****-****-****"); // carte
}
E quando si registra l’input dell’utente:
logger.debug({
message: "raw_user_message",
service: "app-widget",
trace_id,
user_id,
raw: sanitize(userMessage),
});
Questo codice è lontano dal livello industriale, ma rende bene l’idea: prima si pulisce, poi si logga.
10. Pratica: evento gift_recommended per GiftGenius
Ora facciamo l’esercizio: progettiamo l’evento di log gift_recommended, scritto quando GiftGenius sceglie definitivamente il «regalo top» per l’utente.
L’evento deve permettere di rispondere a:
- quale utente (ID interno);
- quale regalo (SKU);
- con quale scenario e a quale passo;
- quale trace_id, per collegarlo agli altri log.
E al tempo stesso non deve contenere PII o segreti.
Esempio:
{
"timestamp": "2025-11-21T10:22:33.456Z",
"level": "info",
"service": "agent",
"env": "prod",
"message": "gift_recommended",
"trace_id": "a3b9e8c2-1f47-4ec5-9bdf-9d4e0c123abc",
"agent_run_id": "run_7f1d2c",
"user_id": "u_123456",
"flow": "gift_recommendation",
"step": "final_choice",
"recommended_sku": "SKU-SPACE-MUG-001",
"price_cents": 2499,
"currency": "USD",
"reason_summary": "recipient_likes_space_and_practical_gadgets"
}
Punti importanti:
- Logghiamo user_id, ma non email e non nome;
- SKU e prezzo sono dati di business normali, non considerati PII;
- reason_summary è un tag tecnico breve, non la frase completa dell’utente;
- ci sono trace_id e agent_run_id, così da poter vedere quali tool ha chiamato l’agente lungo il percorso verso questa scelta.
Cosa non va registrato:
- il testo completo della risposta del modello con la «spiegazione umana»;
- il prompt dell’utente («voglio un regalo per una collega, ha questo telefono, a questo indirizzo»);
- qualsiasi dato di pagamento.
11. Esempi di log: tool‑call riuscita ed errore ACP
Per fissare le idee — due piccoli esempi JSON.
tools.call riuscita sul MCP
{
"timestamp": "2025-11-21T10:20:00.000Z",
"level": "info",
"service": "mcp",
"env": "prod",
"message": "tools.call completed",
"trace_id": "a3b9e8c2-1f47-4ec5-9bdf-9d4e0c123abc",
"request_id": "req_01JCQ5CZ0YQ6TM7E5W8H3N3F2Y",
"tool_name": "suggest_gifts",
"user_id": "u_123456",
"flow": "gift_recommendation",
"step": "rank_candidates",
"result_count": 12,
"latency_ms": 430
}
Da un singolo log del genere si vede già:
- quale tool;
- per quale utente;
- con quale scenario;
- quanto tempo è servito e quanti candidati sono tornati.
Con trace_id trovate facilmente i log della UI e dell’agente relativi alla stessa richiesta.
Errore ACP/checkout
{
"timestamp": "2025-11-21T10:21:05.789Z",
"level": "error",
"service": "commerce",
"env": "prod",
"message": "checkout failed",
"trace_id": "a3b9e8c2-1f47-4ec5-9bdf-9d4e0c123abc",
"checkout_session_id": "cs_test_9YpQvJH8",
"user_id": "u_123456",
"flow": "checkout",
"step": "charge_customer",
"error_type": "upstream",
"error_code": "PAYMENT_DECLINED",
"error_message": "payment provider declined card",
"provider": "stripe",
"amount_cents": 2499,
"currency": "USD"
}
Di nuovo, nessun numero di carta, solo un codice d’errore e un messaggio sicuro. E sempre lo stesso trace_id, quindi potete collegare questo log a gift_recommended e capire in quale punto si è rotta la catena.
12. Come non trasformare i log in spazzatura
È molto allettante: «visto che sappiamo loggare tutto bene, logghiamo proprio tutto». Così vi ritroverete presto con gigabyte di rumore JSON, in cui gli eventi utili si perdono.
Alcuni consigli pratici:
- I log duplicati «sono entrato nella funzione X» senza informazioni aggiuntive servono a poco. Meglio loggare eventi significativi: inizio/fine scenario, chiamata a API esterne, passaggio di step del workflow, errori.
- Per operazioni frequenti (per es., richieste al catalogo prodotti) potete abilitare il sampling: loggare 1 richiesta su N completa, e le altre — solo in caso di errore.
- In produzione tenete DEBUG disattivato (o molto selettivo). Se proprio dovete loggare prompt/risposte — fatelo in modo limitato e con scrub.
Parleremo a parte di metriche e SLO nella prossima lezione, ma è già importante capire: i log non sono solo «per il debug», sono il fondamento dell’osservabilità dell’intero stack ChatGPT.
Vi ricordate il product manager di inizio lezione con la «lista vuota» e il checkout che cade? Con lo schema di log descritto avreste trovato in pochi minuti tutte le richieste con il trace_id desiderato, avreste guardato suggest_gifts (quanti candidati ha restituito il tool, a quale passo è fallito) e i log "checkout failed" con error_code dal fornitore di pagamento. Non più un’indagine nella «poltiglia dei log», ma uno scenario chiaro «dalla richiesta al webhook».
In definitiva, un buon stack di logging per ChatGPT App non è «scriviamo qualcosa su stdout», ma:
- punti corretti di nascita del trace_id (nell’MCP‑gateway/server per i tool «stretti» e all’ingresso del backend per i fetch() del widget negli scenari «ampi»);
- un trace_id unico attraverso App → MCP → commerce → webhook per ogni chiamata di business significativa;
- uno schema comune dei log JSON (service, env, user_id, flow, step, tool_name, ecc.);
- gestione accurata di PII e segreti (scrub, mascheramento, DEBUG limitato in produzione);
- livelli di log sensati e assenza di rumore.
Con questa base, tutti gli altri strumenti di osservabilità (metriche, SLO, alert) diventano molto più utili e aiutano non solo a «raccogliere log», ma a gestire davvero qualità e stabilità della vostra ChatGPT App.
13. Errori tipici nell’uso dei log strutturati e della correlazione
Errore n. 1: assenza di un trace_id unico tra tutti i servizi.
Caso classico: l’MCP‑gateway genera un ID, il backend commerce un altro, i webhook non sanno nulla della correlazione e nei log del widget il trace_id non compare. Risultato: la correlazione diventa una ricerca manuale «sembra che i tempi coincidano». L’approccio corretto è generare il trace_id nei punti di ingresso sotto controllo (server MCP per i tool «stretti», backend/gateway per i fetch() del widget) e portarlo attraverso tutti i confini: header HTTP, campi JSON, contesto dell’agente.
Errore n. 2: tentare di generare il trace_id nel widget e considerarlo «verità».
Può sembrare logico: «facciamo subito in React crypto.randomUUID() e lo mettiamo negli header». Il problema è che così il trace_id vive sul client e può non coincidere con il tracing reale sul server (OpenTelemetry, gateway, altri servizi). Molto più affidabile che il trace_id nasca dove controllate l’intero percorso server: backend Next.js, API gateway o server MCP. Il widget, se vuole, può solo leggerlo e loggarlo.
Errore n. 3: loggare PII e segreti «per comodità di debug».
All’inizio dello sviluppo è «comodissimo» scrivere nel log l’intero corpo del prompt, token, numeri di carta ed email. Dopo qualche mese diventa una bomba a orologeria: l’accesso ai log è tossico, l’audit di sicurezza fa domande scomode e voi avete paura perfino di mostrare uno screenshot dell’errore. Fin da subito implementate lo scrub e non loggate ciò che domani dovrete rimuovere in fretta e furia.
Errore n. 4: log testuali senza struttura in uno dei layer.
A volte il team fa ottimi log JSON in MCP e commerce, ma nel widget lascia console.log("step 1", data). Il risultato è che l’inizio e la fine della catena restano scollegati.
Errore n. 5: abuso del livello ERROR.
Se qualunque deviazione minore (tipo «il modello ha restituito 0 candidati, mostriamo un fallback») viene loggata come ERROR, gli alert in produzione saranno sempre accesi. Il team smette in fretta di reagire agli alert. Cercate di distinguere onestamente: «WARN — è strano, ma abbiamo gestito; ERROR — lo scenario utente è veramente rotto».
Errore n. 6: schemi di log non allineati tra i servizi.
Quando in un servizio il campo si chiama traceId, in un altro correlation_id e in un terzo requestId, nessun sistema di log vi salverà. È importante concordare uno schema unico (come abbiamo fatto con LogEvent) e rispettarlo in tutti i componenti: widget dell’App, server MCP, agenti, ACP, webhook. Così costruire dashboard end‑to‑end e investigare incidenti diventa questione di minuti, non di giorni.
Errore n. 7: «ottimizzare» la dimensione dei log eliminando campi chiave.
A volte, per risparmiare spazio, qualcuno decide: «togliamo user_id o flow, tanto non sono importanti». Poi all’improvviso serve rispondere alla domanda «per quali utenti il checkout fallisce più spesso?» — e scoprite che l’informazione non c’è. Se dovete scegliere cosa eliminare, eliminate i payload testuali lunghi (corpi di richieste/risposte) e i campi di debug, non gli identificatori e gli attributi contestuali chiave.
GO TO FULL VERSION