SDK Manual Instrumentation: Creare Span, Metriche e Eventi Custom
Mentre l'auto-instrumentation fornisce visibilità sulle chiamate infrastrutturali (HTTP, database, messaging), l'instrumentazione manuale con l'SDK permette di aggiungere contesto di business alle tracce, creare metriche personalizzate e registrare eventi significativi per il dominio applicativo. E il passo che trasforma l'observability da strumento tecnico a strumento di analisi di business.
Con l'SDK manuale, puoi rispondere a domande come: "Quanto tempo impiega la validazione dell'inventario per ordini premium?", "Quanti pagamenti vengono rifiutati per tipo di carta?", "Qual e la distribuzione della dimensione degli ordini per regione?". Queste informazioni sono invisibili all'auto-instrumentation.
Cosa Imparerai in Questo Articolo
- Creare span personalizzati con attributi di business
- Gestire il contesto e la propagazione degli span
- Registrare eccezioni e impostare lo status degli span
- Definire metriche custom: Counter, Histogram e Gauge
- Aggiungere eventi puntuali agli span
- Pattern avanzati: span enrichment e context injection
Creare Span Personalizzati
Gli span personalizzati rappresentano operazioni di business significative che non vengono catturate dall'auto-instrumentation. Ogni span ha un nome, attributi, una durata e uno status. La chiave e scegliere la granularità giusta: troppi span creano rumore, troppo pochi non danno visibilità.
from opentelemetry import trace
from opentelemetry.trace import StatusCode
tracer = trace.get_tracer("order-service", "2.0.0")
class OrderProcessor:
def process_order(self, order_request):
"""Processa un ordine con span personalizzati per ogni fase"""
with tracer.start_as_current_span(
"process-order",
attributes={
"order.type": order_request.type,
"order.items_count": len(order_request.items),
"order.total_amount": order_request.total,
"order.currency": order_request.currency,
"customer.id": order_request.customer_id,
"customer.tier": order_request.customer_tier,
}
) as root_span:
# Fase 1: Validazione
with tracer.start_as_current_span(
"validate-order",
attributes={
"validation.rules_count": 8,
"validation.type": "comprehensive"
}
) as validation_span:
errors = self.validate(order_request)
validation_span.set_attribute("validation.errors_count", len(errors))
if errors:
validation_span.set_status(StatusCode.ERROR, "Validation failed")
validation_span.set_attribute("validation.error_types",
",".join([e.type for e in errors]))
raise ValidationError(errors)
# Fase 2: Verifica inventario
with tracer.start_as_current_span(
"check-inventory",
attributes={
"inventory.items_to_check": len(order_request.items),
"inventory.warehouse": order_request.preferred_warehouse
}
) as inv_span:
availability = self.check_stock(order_request.items)
inv_span.set_attribute("inventory.all_available",
all(a.available for a in availability))
inv_span.set_attribute("inventory.backorder_count",
sum(1 for a in availability if not a.available))
# Fase 3: Calcolo prezzo finale
with tracer.start_as_current_span("calculate-pricing") as pricing_span:
pricing = self.calculate_price(order_request)
pricing_span.set_attribute("pricing.subtotal", pricing.subtotal)
pricing_span.set_attribute("pricing.discount_applied", pricing.discount > 0)
pricing_span.set_attribute("pricing.discount_amount", pricing.discount)
pricing_span.set_attribute("pricing.tax", pricing.tax)
pricing_span.set_attribute("pricing.final_total", pricing.total)
# Impostare il risultato sullo span root
root_span.set_attribute("order.id", order.id)
root_span.set_attribute("order.status", "created")
root_span.set_status(StatusCode.OK)
return order
Gestione delle Eccezioni
La registrazione corretta delle eccezioni negli span e fondamentale per il debugging. OTel fornisce
il metodo record_exception che cattura automaticamente tipo, messaggio e stacktrace
dell'errore, salvandoli come evento dello span.
def process_payment(self, order, payment_info):
with tracer.start_as_current_span(
"process-payment",
attributes={
"payment.method": payment_info.method,
"payment.amount": order.total,
"payment.currency": order.currency
}
) as span:
try:
# Tentativo di addebito
result = self.payment_gateway.charge(
amount=order.total,
currency=order.currency,
method=payment_info
)
span.set_attribute("payment.transaction_id", result.tx_id)
span.set_attribute("payment.processor_response", result.response_code)
span.set_status(StatusCode.OK)
return result
except PaymentDeclinedException as e:
# Registrare eccezione con attributi aggiuntivi
span.record_exception(e, attributes={
"payment.decline_code": e.decline_code,
"payment.decline_reason": e.reason,
"payment.retry_eligible": e.retry_eligible
})
span.set_status(StatusCode.ERROR, f"Payment declined: {e.decline_code}")
# Aggiungere evento di retry se applicabile
if e.retry_eligible:
span.add_event("payment.retry.scheduled", {
"retry.delay_seconds": 5,
"retry.max_attempts": 3
})
raise
except GatewayTimeoutError as e:
span.record_exception(e)
span.set_status(StatusCode.ERROR, "Payment gateway timeout")
span.set_attribute("payment.timeout_ms", e.timeout_ms)
raise
except Exception as e:
span.record_exception(e)
span.set_status(StatusCode.ERROR, f"Unexpected error: {type(e).__name__}")
raise
Metriche Custom con l'API Metrics
Le metriche custom permettono di tracciare indicatori di business che non sono disponibili tramite l'auto-instrumentation. OTel supporta tre tipi di strumenti metrici, ciascuno adatto a scenari diversi.
from opentelemetry import metrics
meter = metrics.get_meter("order-service", "2.0.0")
# --- COUNTER: valori che solo aumentano ---
orders_created = meter.create_counter(
name="orders.created.total",
description="Numero totale di ordini creati",
unit="1"
)
revenue_total = meter.create_counter(
name="orders.revenue.total",
description="Ricavo totale degli ordini",
unit="EUR"
)
# --- HISTOGRAM: distribuzione di valori ---
order_processing_duration = meter.create_histogram(
name="orders.processing.duration",
description="Durata del processing degli ordini",
unit="ms"
)
order_items_count = meter.create_histogram(
name="orders.items.count",
description="Numero di articoli per ordine",
unit="1"
)
# --- UP_DOWN_COUNTER: valori che possono aumentare o diminuire ---
pending_orders = meter.create_up_down_counter(
name="orders.pending.count",
description="Ordini attualmente in fase di elaborazione",
unit="1"
)
# --- Utilizzo delle metriche ---
def on_order_created(order):
# Counter: incrementare con attributi
orders_created.add(1, {
"order.type": order.type,
"customer.tier": order.customer_tier,
"payment.method": order.payment_method,
"region": order.shipping_region
})
# Counter: registrare il ricavo
revenue_total.add(order.total, {
"order.type": order.type,
"currency": order.currency
})
# Histogram: registrare la durata
order_processing_duration.record(order.processing_time_ms, {
"order.type": order.type,
"customer.tier": order.customer_tier
})
# Histogram: registrare il numero di articoli
order_items_count.record(len(order.items), {
"order.type": order.type
})
def on_order_processing_started(order):
pending_orders.add(1, {"order.type": order.type})
def on_order_processing_completed(order):
pending_orders.add(-1, {"order.type": order.type})
Eventi degli Span: Log Puntuali nel Contesto
Gli span events sono log puntuali associati a uno span specifico, con timestamp e attributi. Sono ideali per registrare momenti significativi durante un'operazione senza creare span separati per ogni passaggio.
// Java: utilizzo avanzato di span events
import io.opentelemetry.api.trace.Span;
public class FraudDetectionService {
public FraudCheckResult checkFraud(Order order) {
Span span = Span.current();
// Evento: inizio analisi
span.addEvent("fraud.analysis.started", Attributes.of(
AttributeKey.stringKey("fraud.model_version"), "v3.2.1",
AttributeKey.longKey("fraud.rules_count"), 42L
));
// Fase 1: controllo velocità
double velocityScore = checkVelocity(order);
span.addEvent("fraud.velocity_check.completed", Attributes.of(
AttributeKey.doubleKey("fraud.velocity_score"), velocityScore,
AttributeKey.booleanKey("fraud.velocity_passed"), velocityScore < 0.7
));
// Fase 2: controllo geo
double geoScore = checkGeolocation(order);
span.addEvent("fraud.geo_check.completed", Attributes.of(
AttributeKey.doubleKey("fraud.geo_score"), geoScore,
AttributeKey.stringKey("fraud.geo_country"), order.getShippingCountry()
));
// Fase 3: ML model
double mlScore = runMLModel(order);
span.addEvent("fraud.ml_model.completed", Attributes.of(
AttributeKey.doubleKey("fraud.ml_score"), mlScore,
AttributeKey.stringKey("fraud.ml_decision"),
mlScore > 0.8 ? "block" : mlScore > 0.5 ? "review" : "pass"
));
// Risultato finale
double finalScore = (velocityScore + geoScore + mlScore) / 3;
span.setAttribute("fraud.final_score", finalScore);
span.setAttribute("fraud.decision",
finalScore > 0.7 ? "blocked" : finalScore > 0.4 ? "review" : "approved");
return new FraudCheckResult(finalScore);
}
}
Context Management: Propagazione Manuale
In scenari complessi come thread pool, async/await o code interne, il contesto dello span potrebbe non propagarsi automaticamente. In questi casi serve la gestione manuale del contesto.
from opentelemetry import trace, context
from opentelemetry.context import attach, detach
import concurrent.futures
tracer = trace.get_tracer("order-service")
def process_orders_in_parallel(orders):
with tracer.start_as_current_span("batch-process-orders") as parent_span:
parent_span.set_attribute("batch.size", len(orders))
# Catturare il contesto corrente per propagarlo ai thread
ctx = context.get_current()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = []
for order in orders:
# Passare il contesto al thread worker
future = executor.submit(
process_single_order_with_context,
order,
ctx
)
futures.append(future)
# Attendere tutti i risultati
results = [f.result() for f in futures]
parent_span.set_attribute("batch.completed", len(results))
return results
def process_single_order_with_context(order, parent_ctx):
# Attaccare il contesto del genitore in questo thread
token = attach(parent_ctx)
try:
with tracer.start_as_current_span(
"process-single-order",
attributes={"order.id": order.id}
) as span:
result = do_processing(order)
span.set_attribute("order.status", result.status)
return result
finally:
# Ripristinare il contesto originale del thread
detach(token)
Best Practice per l'Instrumentazione Manuale
Scegli nomi significativi: usa verbi che descrivono l'operazione di business
(validate-order, check-inventory, process-payment), non
nomi tecnici generici (step1, process).
Aggiungi attributi all'inizio e alla fine: attributi noti subito (tipo ordine,
customer tier) all'inizio; risultati (order ID, status) alla fine.
Non creare span per ogni riga di codice: un buon span rappresenta un'operazione
logica completa (validazione, pagamento, notifica), non un singolo if/else.
Gestisci sempre le eccezioni: ogni try/catch dovrebbe avere record_exception
e set_status(ERROR) per rendere gli errori visibili nelle tracce.
Conclusioni e Prossimi Passi
L'instrumentazione manuale con l'SDK OpenTelemetry trasforma le tracce da timeline tecniche a strumenti di analisi di business. Span personalizzati, attributi domain-specific, metriche custom e eventi puntuali forniscono il contesto necessario per rispondere a domande che l'auto-instrumentation non può affrontare.
La combinazione di auto-instrumentation (per la copertura infrastrutturale) e instrumentazione manuale (per il contesto di business) e l'approccio ottimale per un'observability completa. La chiave e la granularità: instrumentare le operazioni significative senza creare rumore con span eccessivi.
Nel prossimo articolo esploreremo l'OTel Collector, il componente centrale del pipeline di telemetria che riceve, processa e esporta i dati verso i backend di storage e visualizzazione.







