ENbext.dev
DocumentationFonctionnalités

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Traceur

La capacité Traçage est la forme que bext utilise pour émettre des traçages. Le runtime enregistre un délai chaque fois qu'une requête entre dans un gestionnaire, lorsqu'un plugin effectue un travail intéressant ou lorsqu'une fonction hôte termine une opération qui mérite d'être chronométrée ; le TracerPlugin actif décide où vont ces travées. Échangez le plugin et vos journaux atterrissent dans stdout, ou OTLP, ou Honeycomb, ou Datadog – sans aucune modification du code qui a produit les étendues.

Cette page présente le trait, explique pourquoi il est basé sur un handle au lieu de prendre Box<dyn Span>, montre à quoi sert la sentinelle SpanHandle::INVALID, note comment la forme s'aligne avec les conventions sémantiques OpenTelemetry sans dépendre de la caisse opentelemetry et se termine par un court exemple de bout en bout d'un plugin émettant une étendue personnalisée.

Le trait#

La forme complète réside dans bext-plugin-api::tracer. Il est synchronisé, sécurisé pour les objets et ne comporte aucun générique sur le trait lui-même :

rust
pub trait TracerPlugin: Send + Sync {
    fn name(&self) -> &str;

    fn start_span(&self, span: SpanStart<'_>) -> SpanHandle;
    fn set_attribute(&self, span: SpanHandle, key: &str, value: AttrValue);
    fn set_status(&self, span: SpanHandle, status: SpanStatus);
    fn add_event(&self, span: SpanHandle, event: SpanEvent<'_>);
    fn end_span(&self, span: SpanHandle, end_time_unix_nanos: Option<u64>);

    fn flush(&self) -> Result<(), String>;
}

Chaque méthode d'enregistrement — start_span, set_attribute, set_status, add_event, end_span — est infaillible. Un traceur qui ne peut pas enregistrer une étendue doit la supprimer silencieusement, et non propager une erreur via un code qui n'a jamais demandé d'en gérer une. La seule méthode faillible est flush, car flush est une véritable limite d'E/S où l'appelant veut savoir si le backend a accepté le lot.

start_span prend un SpanStart<'_> — le nom, le type d'étendue, un descripteur parent facultatif, les attributs initiaux et un horodatage de début explicite facultatif en nanosecondes Unix. Passer un parent explicitement, plutôt que de l'extraire d'un thread local, permet d'appeler le trait en toute sécurité à partir de n'importe quel modèle d'exécution. bext exécute des plugins dans : code hôte de synchronisation, tâches asynchrones, WASM, QuickJS, selon le cas.

Pourquoi des poignées, pas Box<dyn Span>#

La conception alternative évidente est celle que OpenTelemetry lui-même utilise : start_span renvoie un objet de trait encadré et vous appelez .set_attribute(...) sur cet objet. Nous avons essayé cette forme et l'avons rejetée pour trois raisons.

Ce n'est pas Send + Sync proprement sur FFI. Les plugins exécutés dans QuickJS ou WASM doivent stocker une référence au span quelque part et y revenir lors d'un appel différent vers l'hôte. Traverser la frontière FFI avec un objet de trait qui possède un état signifie l'envelopper dans Arc<Mutex<dyn Span + Send + Sync>>, ce qui est bruyant dans chaque liaison de langage et transfère les règles de propriété de Rust dans le runtime invité.

Il oblige le backend à gérer les durées de vie. Un plugin qui oublie de terminer un span — ou panique à mi-chemin — laisse échapper un span en boîte sur le tas du traceur. Grâce aux poignées, le traceur conserve un HashMap<SpanHandle, InternalSpan> et peut récolter les entrées obsolètes selon son propre calendrier.

Il confond identité et stockage. Une étendue a une identité stable (sa paire (trace_id, span_id)) qui est significative au-delà des limites du processus, et une représentation locale qui ne l'est pas. Les handles séparent les deux : le handle est une valeur Copy simple que vous pouvez enregistrer, transmettre via FFI ou utiliser pour rechercher le même span à partir d'un appel hôte complètement différent.

Donc SpanHandle est exactement l’identité :

rust
pub struct SpanHandle {
    pub trace_id: [u8; 16],
    pub span_id: [u8; 8],
}

16 octets d'identifiant de trace, 8 octets d'identifiant de span. Il s'agit des largeurs exactes utilisées par OpenTelemetry et W3C Trace Context, ce qui signifie qu'un handle est un identifiant de fil valide sans aucune traduction.

La sentinelle INVALIDE#

Chaque SpanHandle est Copy et il existe une valeur distincte :

rust
impl SpanHandle {
    pub const INVALID: SpanHandle = SpanHandle {
        trace_id: [0u8; 16],
        span_id: [0u8; 8],
    };

    pub fn is_invalid(&self) -> bool {
        self.trace_id == [0u8; 16] && self.span_id == [0u8; 8]
    }
}

Il s'agit de la valeur renvoyée par un traceur lorsqu'il décide de ne pas enregistrer l'intervalle — par exemple, parce que l'échantillonnage l'a supprimé, parce que le traceur s'arrête ou parce que le plugin a transmis un parent qui n'existait pas. Toutes les méthodes d'enregistrement vérifient is_invalid en haut et reviennent immédiatement, donc appeler set_attribute sur un handle non valide est une non-opération garantie.

Le résultat est que le code du plugin n'a jamais besoin de bifurquer sur « le traceur a-t-il décidé d'enregistrer cela ? Le code du chemin heureux est le même, que la durée soit réelle ou non :

rust
let span = tracer.start_span(SpanStart {
    name: "checkout.verify_stock",
    kind: SpanKind::Internal,
    ..Default::default()
});
tracer.set_attribute(span, "cart.items", AttrValue::from(items as i64));
// ... do the work ...
tracer.set_status(span, SpanStatus::Ok);
tracer.end_span(span, None);

Si l'échantillonnage a réduit l'intervalle, chaque ligne après start_span est une non-opération. Pas d'échelle if let Some(span), pas de Option<SpanHandle> traversant l'arborescence des appels.

S'aligne avec OpenTelemetry sans en dépendre#

Le trait utilise des types primitifs simples - tableaux d'octets, tranches de chaîne, String appartenant et une petite énumération AttrValue - et n'importe délibérément rien de la caisse opentelemetry. Il s'agit d'une règle stricte : bext-plugin-api doit rester indépendant du fournisseur, car une surface de fonctionnalités stable est effectivement permanente. Coupler le trait à une version d’une caisse épinglerait pour toujours l’ensemble de l’écosystème à ce choix.

Dans le même temps, la forme s'aligne exactement sur les conventions sémantiques OpenTelemetry, de sorte que le backend OTLP peut mapper les types un à un sans rien inventer :

Type bext équivalent à OpenTelemetry
SpanHandle.trace_id TraceId 16 octets
SpanHandle.span_id SpanId 8 octets
SpanKind opentelemetry::trace::SpanKind
SpanStatus opentelemetry::trace::Status
AttrValue opentelemetry::Value
start_time_unix_nanos portée start_time comme SystemTime

Le plugin de référence @bext/tracer-otlp est le seul endroit dans l'espace de travail qui importe les noms opentelemetry::*. Il les convertit vers et depuis les formes primitives simples de bext à la limite du trait et transmet le tout au SDK otel situé en dessous.

Les deux plugins de référence#

@bext/tracer-stdout#

crates/bext-impls/bext-tracer-stdout est le traceur de développement. Il écrit un objet JSON par span terminé sur la sortie standard (ou tout récepteur Write + Send que vous branchez), sans aucune dépendance au-delà de serde_json. Le récepteur est un SharedSink = Arc<Mutex<dyn Write + Send>>, ce qui permet de construire des tests avec un tampon Vec<u8> partagé à moindre coût - la propre suite de tests de la caisse fait exactement cela pour affirmer ce que le traceur a émis.

start_span alloue un identifiant de trace en regroupant un compteur atomique avec l'heure Unix-nano actuelle et un identifiant d'étendue du même compteur, puis stocke l'étendue en cours dans un Mutex<HashMap<SpanHandle, SpanInternal>>. end_span supprime l'entrée, remplit end_time et duration et sérialise une ligne vers le récepteur. flush est Ok(()) — la sortie standard est déjà vidée ligne par ligne.

@bext/tracer-otlp#

crates/bext-impls/bext-tracer-otlp est le traceur de production. Il enveloppe opentelemetry_sdk::trace::SdkTracerProvider et expose deux constructeurs : un qui prend un fournisseur entièrement assemblé (afin que les appelants puissent connecter eux-mêmes le traitement par lots, les échantillonneurs, les attributs de ressources et plusieurs exportateurs) et un qui prend n'importe quel SpanExporter et construit un fournisseur simple autour de lui. La deuxième forme est celle utilisée par les propres tests de la caisse, avec un MemExporter en mémoire qui ajoute chaque étendue exportée à un Vec<SpanData> partagé au lieu de l'envoyer via le réseau.

À l'intérieur, start_span transforme le handle parent facultatif en opentelemetry::trace::SpanContext, l'enveloppe dans un Context et crée l'étendue via le SDK. Le opentelemetry_sdk::trace::Span renvoyé réside dans le même modèle Mutex<HashMap<SpanHandle, _>> que la version stdout, et end_span / flush est transmis au SDK. Flush mappe toute erreur de force_flush à un String afin qu'elle traverse proprement la limite du trait.

Configuration#

Choisir quel traceur est actif est une ligne dans bext.config.toml :

toml
[tracer]
exporter = "otlp"
endpoint = "http://localhost:4317"
service_name = "bext-edge-01"
sample_ratio = 1.0

[tracer.resource_attributes]
"deployment.environment" = "production"
"service.version" = "2.3.1"

Remplacez exporter = "otlp" par exporter = "stdout" pendant le développement et vous obtenez du JSON orienté ligne sur la sortie standard du processus – même trait, mêmes étendues, backend différent. Pour les configurations multi-locataires ou la répartition vers plusieurs collecteurs, pointez sur un SdkTracerProvider préassemblé dans Rust et transmettez-le directement à OtlpTracerPlugin::with_provider ; le chemin piloté par la configuration est pratique pour le cas courant.

Émettre un span à partir d'un plugin#

Les plugins ont accès au traceur actif via la table de fonctions hôte ( host.tracer() ), qui renvoie un &dyn TracerPlugin. Voici un exemple minimal de plugin encapsulant une unité de travail dans sa propre étendue :

rust
use bext_plugin_api::tracer::{AttrValue, SpanKind, SpanStart, SpanStatus};

pub fn handle_checkout(host: &Host, cart_id: &str, items: usize) -> Result<(), String> {
    let tracer = host.tracer();
    let span = tracer.start_span(SpanStart {
        name: "checkout.handle",
        kind: SpanKind::Internal,
        attributes: vec![
            ("cart.id".into(), AttrValue::from(cart_id)),
            ("cart.items".into(), AttrValue::from(items as i64)),
        ],
        ..Default::default()
    });

    let result = verify_stock(host, cart_id);

    match &result {
        Ok(_) => tracer.set_status(span, SpanStatus::Ok),
        Err(e) => tracer.set_status(span, SpanStatus::Error {
            description: e.clone(),
        }),
    }
    tracer.end_span(span, None);
    result
}

Trois choses à noter. Premièrement, le plugin ne sait pas à quel traceur il s'adresse - le code est identique, que le plugin actif soit stdout, otlp ou quelque chose que l'opérateur a écrit lui-même. Deuxièmement, la queue ..Default::default() récupère des valeurs par défaut raisonnables pour le parent, l'heure de début et les attributs initiaux vides, de sorte que les étendues simples restent courtes. Troisièmement, le bras Err appelle toujours end_span, car la fin de la durée est ce qui libère l'entrée dans la carte des poignées du traceur - en oubliant qu'il s'agit du seul bug réel que la conception de la poignée peut produire.

Attention

Appelez toujours end_span sur chaque chemin de code, y compris les chemins d'erreur. Une période qui n'est jamais terminée laisse filtrer une entrée dans la carte de handles interne du traceur pendant toute la durée de vie du processus.

Où aller ensuite#

  • Le aperçu des capacités couvre l'échelle de promotion que la capacité de traceur parcourra une fois qu'elle atterrira sous le @bext/.
  • Observabilité montre comment les étendues intégrées du serveur circulent à travers le traceur actif sans aucun code de plug-in.
  • Construire un plugin parcourt l'échafaudage d'un plugin qui peut déclarer requires_capabilities = ["tracer"] dans son manifeste et appeler host.tracer() au moment de l'exécution.
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.