Aller au contenu

Observation

piighost émet une trace OpenTelemetry à chaque dé-identification. Chaque appel ouvre un span racine et un span enfant par étape du pipeline. On voit ainsi où une PII a été détectée, comment elle a été liée, quel token l'a remplacée et si le guard rail a laissé passer. Le traçage est optionnel et n'est jamais requis pour dé-identifier.

Note

Les payloads des traces portent par défaut les valeurs de PII en clair, donc une trace fait aussi office de jeu de données d'annotation. Passez un observation_redactor pour caviarder ces valeurs avant d'envoyer les traces vers un backend en qui vous n'avez pas pleine confiance. Voir Caviarder les payloads des traces plus bas.

La couture du tracer

Le pipeline ne parle jamais directement à un backend de traçage. Il appelle get_tracer() une fois à la construction, puis enregistre via le tracer retourné.

from piighost.observation import get_tracer

tracer = get_tracer()
with tracer.span("piighost.detect") as span:
    span.set_input(text)
    span.set_output(detections)
    span.set_attribute("count", len(detections))

get_tracer() retourne un tracer basé sur OpenTelemetry quand l'extra observation est installé, et un tracer no-op sinon. Le tracer no-op n'enregistre rien et ne coûte rien, donc le pipeline émet ses spans sans condition, sans garde autour de chaque appel. Contrairement aux autres dépendances optionnelles, un extra manquant dégrade vers le tracer no-op au lieu de lever une exception, parce que le traçage ne doit jamais bloquer le dé-identification.

Un span est un gestionnaire de contexte qui porte un payload d'entrée, un payload de sortie et des attributs scalaires. L'imbrication est implicite. Un span ouvert à l'intérieur d'un autre devient son enfant via le contexte ambiant d'OpenTelemetry, donc le pipeline ne fait pas transiter de poignée parente entre ses étapes.

Un span par étape

AnonymizationPipeline.anonymize ouvre un span racine piighost.anonymize, puis un span enfant par étape exécutée. Une étape désactivée n'émet aucun span. L'arbre d'un run complet est le suivant.

flowchart TD
    A[piighost.anonymize] --> B[piighost.detect]
    A --> C[piighost.override]
    A --> D[piighost.overlap]
    A --> E[piighost.expand]
    A --> F[piighost.link]
    A --> G[piighost.entity_resolve]
    A --> H[piighost.render]
    A --> I[piighost.guard]

L'arbre des spans d'une dé-identification. Les étapes optionnelles n'apparaissent que si elles sont configurées.

Le span racine enregistre le texte d'entrée et le texte dé-identifié final. detect enregistre les détections et leur nombre. link enregistre les entités. render enregistre le texte dé-identifié et le nombre de tokens. guard enregistre s'il a levé un drapeau et les labels vus. Le pipeline de thread diffère. Il exécute la résolution de chevauchement et l'expansion dans _detect et la résolution d'entités dans _thread_tokens, si bien qu'aucune de ces étapes n'obtient de span propre, laissant detect, link et render sous la racine. Le span racine et le span detect portent aussi un attribut cache_hit et un langfuse.session.id. Il émet un span piighost.deanonymize quand il restaure un texte.

Les spans s'imbriquent sous le span courant au moment de l'appel anonymize. Ouvrez un span applicatif autour d'une conversation et chaque appel du pipeline se rend en dessous, comme une seule trace.

Caviarder les payloads des traces

Par défaut un payload de span contient la PII en clair. Le span detect enregistre Patrick, le span racine enregistre le texte d'entrée avec Patrick à sa place. C'est délibéré. Une trace avec les valeurs en clair est un jeu de données prêt à l'emploi pour évaluer la qualité de détection.

C'est aussi une fuite si le backend n'a pas à connaître les PII. Passez un observation_redactor, une placeholder factory, au constructeur du pipeline et chaque payload est caviardé au travers avant de sortir du processus.

from piighost.pipeline import AnonymizationPipeline
from piighost.components.placeholder import LabelPlaceholderFactory

redactor = LabelPlaceholderFactory()
pipeline = AnonymizationPipeline(
    detector,
    linker,
    anonymizer,
    observation_redactor=redactor,
)

Avec le redactor défini, le span detect enregistre <<PERSON>> au lieu de Patrick, et le payload d'entrée montre le texte caviardé. Le compromis est direct. Une trace caviardée est sûre à envoyer vers n'importe quel backend mais ne peut plus servir de jeu de données d'annotation, puisque les valeurs en clair ont disparu.

observation_redactor Payloads des traces Sûr pour un backend non fiable Utilisable comme jeu de données
None (défaut) valeurs de PII en clair non oui
une placeholder factory tokens caviardés oui non

Le traçage en clair reste le défaut, pour que les traces gardent leur valeur d'annotation, mais c'est un choix explicite. Sans redactor et avec un tracer provider réellement configuré, le pipeline avertit une fois à la construction que ses traces portent de la PII en clair. Passez trace_clear_text=True pour l'assumer et taire l'avertissement, ou un observation_redactor pour caviarder les payloads.

pipeline = AnonymizationPipeline(
    detector=detector,
    trace_clear_text=True,  # I know traces carry clear PII, ship them to a trusted backend only
)

La corrélation avec un backend est de la configuration de déploiement, pas du code de la lib

piighost émet des spans OpenTelemetry standard et s'arrête là. Il ne fournit aucun adapter par backend. Le backend qui reçoit les spans relève de la configuration du SDK OpenTelemetry de l'application, posée une fois au déploiement, en dehors de piighost.

N'importe quel exporteur OTLP fonctionne tel quel. Les spans atteignent le TracerProvider que l'application a enregistré. Sans provider configuré, l'API OpenTelemetry est un no-op et les spans ne vont nulle part.

Langfuse est une cible courante parce que son SDK v3 est bâti sur OpenTelemetry. Pointez-le vers le processus et il capture les spans piighost à côté des siens. Son filtre d'export par défaut ne laisse passer que ses propres spans et des instrumenteurs LLM connus, donc admettez le scope d'instrumentation piighost via le prédicat should_export_span du SDK.

from langfuse import Langfuse

def export_piighost_spans(span) -> bool:
    scope = span.instrumentation_scope
    if scope is None:
        return False
    return (
        scope.name == "langfuse-sdk"
        or scope.name == "piighost"
        or scope.name.startswith("piighost.")
    )

client = Langfuse(should_export_span=export_piighost_spans)

Les payloads sont sérialisés sous les clés d'attributs que Langfuse mappe vers l'entrée et la sortie d'une observation, donc ils s'y rendent richement. N'importe quel autre backend OTLP les montre comme de simples attributs de span. Rien de tout cela ne vit dans piighost, c'est le câblage SDK que vous faites déjà pour le reste de votre stack.

La version complète et exécutable, avec repli console quand aucun credential Langfuse n'est présent, est dans examples/observation/langfuse_tracing.py.

Voir aussi