Salta al contenuto principale

05 - SDK Manual Instrumentation - Creating Spans, Metrics, and Custom Events

OTel SDK setup, creating spans programmatically, metric recording, attributes, events, context management, Java/Python examples.

Cosa ne pensi di questo articolo?

05 - SDK Manual Instrumentation - Creating Spans, Metrics, and Custom Events
ObservabilityFoto:AS PhotographyPexels

In Breve

OTel SDK setup, creating spans programmatically, metric recording, attributes, events, context management, Java/Python examples.

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.

Domande frequenti

Come si differenzia l'instrumentazione manuale con l'SDK rispetto all'auto-instrumentation?

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, mentre l'auto-instrumentation fornisce visibilità sulle chiamate infrastrutturali.

Qual è la chiave per creare span personalizzati?

La chiave è scegliere la granularità giusta: troppi span creano rumore, troppo pochi non danno visibilità.

Come si gestiscono le eccezioni negli span?

La registrazione corretta delle eccezioni negli span e fondamentale per il debugging. OTel fornisce il metodo che cattura automaticamente tipo, messaggio e stacktrace dell'errore, salvandoli come evento dello span.

Perché è importante gestire manualmente il contesto degli span?

In scenari complessi come thread pool, async/await o code interne, il contesto dello span potrebbe non propagarsi automaticamente.

Fonti e approfondimenti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti