Skip to content

Observabilité et audit ​

Journaux et traces d’appels servent à analyser erreurs, latence et usage modèle ; le journal d’audit enregistre les changements de ressources ; le tableau de bord des files inspecte l’arriéré, les échecs et les retries des tâches de fond.

ObjectifOù regarder
Ce qu’une Q&R a récupéré, combien d’appels modèle, tokens consommésChaîne complète dans Langfuse après branchement
Changements base, membres ou paramètres système« Activité » des paramètres de base ; « Paramètres → Journal d’audit »
Parse, résumé, Wiki en file ou en échec« Paramètres → Files runtime »
Service vivantGET /health
Relier les logs au même requestFiltrer logs par X-Request-ID (en-tête de réponse)
Files runtime : arriéré, échecs et retries par file
Files runtime : arriéré, échecs et retries par file
Trace Langfuse : chaîne complète d’une Q&R
Trace Langfuse : chaîne complète d’une Q&R

Pour un incident request : récupérer X-Request-ID dans les en-têtes, puis corréler logs et traces.

Aide-mémoire exploitation ​

BesoinOù
Chaîne complète d’un requestgrep logs app avec X-Request-ID ; avec LLM_DEBUG_LOG → llm_debug/<request_id>.log
Arbre appels LLM et tokens (chat/parse)UI Langfuse (trace POST /api/v1/agent-chat ou asynq.document:process)
Qui a changé quoi et quandAudit espace /tenants/:id/audit-log ; activité KB /knowledge-bases/:id/activity ; audit plateforme /system/admin/audit-log
Pourquoi un document échoue en boucleTable task_dead_letters (scope=knowledge/knowledge_base) + last_error des tâches archivées dans le panneau runtime
Service vivantGET /health (200 {"status":"ok"})
Config chargée comme prévuBannière démarrage [startup-env] (internal/runtime/startup.go, secrets = longueur seulement)

Health check ​

internal/router/router.go enregistre une sonde sans auth (liste blanche publique dans internal/middleware/auth.go inclut /health) :

go
// internal/router/router.go
r.GET("/health", func(c *gin.Context) {
    c.JSON(200, gin.H{"status": "ok"})
})

Sonde liveness pure (sans DB/Redis), adaptée conteneur / LB. langfuse.shouldTrace et l’échantillonnage des logs l’ignorent aussi. Uptime via MarkServerStarted/ServerUptime dans internal/runtime/server.go pour le panneau ops.

Référence logs, traces et audit ​

Système de logs (internal/logger) ​

Format et niveau ​

  • Instance logrus privée (appLogger, évite perte si global modifié), CustomFormatter.
  • Ligne par défaut : LEVEL[timestamp] [request_id…] caller | message, caller fichier:ligne[fonction] (addCaller).
  • Template via WEKNORA_LOG_FORMAT : %d temps, %level, %thread goroutine (lazy), %logger, %traceId request_id, %msg. %msg obligatoire si template non vide ; json non supporté comme mot-clé. Rétrocompat LOG_FORMAT (déprécié) si nouvelle var absente.
  • Niveau LOG_LEVEL (debug/info/warn/error/fatal, défaut debug si absent/invalide).
  • Couleurs ANSI si stdout TTY ; désactivées hors TTY (Docker) ; fichier via ansiStripWriter.
  • Champs structurés : WithField/WithFields dans le context (types.LoggerContextKey) ; WarnWithFields pour audit (sondage cross-tenant, invariant).
  • CloneContext : copie tenant/user/request_id/rôle/langue + trace Langfuse et span OTel actif pour goroutines filles.

Sortie et rotation ​

ConfigureFromEnv() (init + rappel après .env dans main) : toujours stdout ; si LOG_PATH (ou macOS .app → ~/Library/Logs/<App>/<App>.log) → lumberjack :

go
// internal/logger/logger.go openLogFile()
return &lumberjack.Logger{
    Filename:   logPath,
    MaxSize:    50, // megabytes
    MaxBackups: 3,
    MaxAge:     28, // days
    Compress:   true,
}, nil

Logs debug LLM (internal/logger/llm_logger.go) ​

LLM_DEBUG_LOG=true|1|<dir> : chaque appel modèle (Chat / Stream / Embedding / Rerank / VLM) écrit entrées/sorties/outils/erreurs complets dans llm_debug/, un fichier par request_id (<request_id>.log). Nettoyage fichiers >7 jours au démarrage (cleanupOldDebugFiles).

Middleware logs requête (internal/middleware/logger.go) ​

  • RequestID() : lit ou génère X-Request-ID, en-tête réponse, context gin + http.Request — corrélation workers asynq.
  • Logger() : method, path (query sanitizeQuery masque token/code/state OAuth), status, latence, IP, taille, corps req/réponse ≤10KB. sensitiveFieldRegex masque password/token/api_key/secret/private_key ("***") ; SSE → [réponse SSE streaming, ignorée] ; skip /assets/ et stats wiki poll.
  • Proxies de confiance : SetTrustedProxies (WEKNORA_TRUSTED_PROXIES) contre X-Forwarded-For forgé.

Traces Langfuse (internal/tracing/langfuse) ​

OpenTelemetry Go SDK, attributs sémantiques Langfuse (langfuse.observation.*), export OTLP/HTTP POST <host>/api/public/otel/v1/traces (Langfuse v3+, LiteFuse). Traces off par défaut ; pas d’export si désactivé.

Configuration (env, config.go) ​

VariableDéfautDescription
LANGFUSE_ENABLEDauto si clésInterrupteur (aligné SDK Python)
LANGFUSE_HOSThttps://cloud.langfuse.comBase Langfuse/LiteFuse
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY—Basic Auth projet
LANGFUSE_RELEASE / LANGFUSE_ENVIRONMENT—Filtres UI trace
LANGFUSE_FLUSH_AT15Taille lot export (MaxExportBatchSize)
LANGFUSE_FLUSH_INTERVAL3sIntervalle max lot (BatchTimeout)
LANGFUSE_QUEUE_SIZE2048Tampon mémoire
LANGFUSE_REQUEST_TIMEOUT10sTimeout HTTP ingestion
LANGFUSE_SAMPLE_RATE1.0ParentBased(TraceIDRatioBased) 0..1
LANGFUSE_DEBUGfalseLogs erreurs batch

Exporteur (exporter.go) ​

OTLP/HTTP, Authorization: Basic base64(public:secret) ; client HTTP dédié (hors garde SSRF ; pas de whitelist si SSRF_DNS_WHITELIST_ONLY). En-têtes x-langfuse-ingestion-version: 4 (requis v3/LiteFuse OTel), x-langfuse-sdk-name/version. Manager (manager.go) : TracerProvider isolé (service.name=weknora), pas de mutation globale OTel ; propagateur W3C TraceContext privé au package.

Modèle d’observation et instrumentation ​

Trois handles (tracer.go) : Trace (racine request), Span (unité non-LLM), Generation (appel modèle + TokenUsage, streaming MarkCompletionStart). Hiérarchie via context span OTel ; auto-trace si absent pour éviter orphelins.

Principaux points :

PointSourceProduit
Entrée HTTPmiddleware.go GinMiddlewareTrace racine sur chemins shouldTrace (knowledge-chat / agent-chat / knowledge-search / ingestion POST/PUT / import FAQ / wiki auto-fix / evaluation / init…), nom METHOD /path, metadata http + request_id, sortie status/size ; hérite traceparent W3C
Worker asynqasynq.go AsynqMiddlewareReprend traceparent payload ou asynq.<task_type> ; SPAN + metadata task_id/queue/retry… ; payload preview 1KB
Injection enqueueasynq.go InjectTracing + internal/types/tracing.gotraceparent + tags user/session en lf_* dans payload
Appels modèleinternal/models/{chat,embedding,rerank,vlm,asr}/langfuse_wrapper.goGeneration par appel
Résumé retrievalretrieval_obs.goTop-25 preview (rank/chunk_id/score/160 car.)
Agentinternal/agent/engine.go, act.goSPAN agent.execute, CloneContext même arbre HTTP

Attributs span (events.go) : types Langfuse, input/output/metadata, usage_details, user.id (user ou tenant:<id>), session.id, environment/release.

Nœuds retrieval et diagnostic ​

embedding.embed, retrieve, rerank chronomètrent requête, retrieval et rerank. Nœuds diagnostics.* : stats/filtres sur résultats existants, pas d’appel retrieval/modèle supplémentaire.

Nœud diagnosticSignification
diagnostics.retrieval.vector.result_pool.initialPool initial vecteur
diagnostics.retrieval.keywords.result_pool.initialPool initial mots-clés
diagnostics.retrieval.<vector/keywords>.result_pool.refillPool après refill (si refill)
diagnostics.retrieval.<vector/keywords>.filter_decisionPool avant filtre + décision
diagnostics.retrieval.fusion.candidate_selectionTroncature fusion
diagnostics.rerank.selectionDécision post-rerank

Pools : texte, résumé, légendes image, OCR, vecteurs image selon requête. image_samples = échantillon vecteurs image ; metadata observation_kind=diagnostic, sample_scope=image_vectors. Sans refill, initial et filter_decision peuvent montrer le même lot observé à deux étapes.

Renommages n’affectent que les traces nouvelles ; historique garde retrieval.image_*, rerank.selection. Ajuster dashboards filtrés par nom de nœud.

Usage par tour, finalité d’appel et cache ​

Chaque appel = Generation ; usage message = agrégat tour ; événement fin agent via turn_usage ; historique relu. cached_tokens alias de cache_read ; cache_read/write/miss = détail entrée, ne pas additionner à prompt_tokens. cache_reported / cache_status : hit, miss, unreported, unsupported. Metadata call_purpose + empreinte préfixe prompt.

Prompt : instructions stables en tête, session dynamique en queue ; marqueurs cache si provider supporte. Hit dépend protocole/modèle/préfixe. Sandbox : spans Langfuse pour ops instance/outils ; maintenance sandbox = spans enfants si trace parente ; install skills ouvre sa trace. Export off si tracing désactivé.

Journal d’audit ​

Modèle (internal/types/audit_log.go) ​

Table audit_logs append-only (pas UpdatedAt/soft delete), id monotone = PK + curseur :

ChampTypeDescription
iduint64 autoPK + curseur WHERE id < after_id ORDER BY id DESC
tenant_iduint64Espace ; 0 = système
actor_user_id / actor_rolevarcharActeur et rôle au moment de l’action
actionvarchar(64)<area>.<event> (§4.2)
scope_type / scope_idvarcharPortée ressource (ex. knowledge_base + kbID)
target_type / target_id / target_user_idvarcharCible
request_path / request_methodvarcharTemplate route (pas URL brute ; brute dans Details.raw_path)
outcomevarchar(16)success / accepted / denied / failed / partial / canceled
detailsjsonbPayload action ; jamais secrets en clair ; activité KB via clé API → api_key_id / api_key_name
created_attimestampRétention

Actions audit ​

GroupeActions
RBAC / membresrbac.member_added, rbac.member_removed, rbac.member_role_changed, rbac.member_left, rbac.access_denied, rbac.invitation_sent, rbac.invitation_accepted, rbac.invitation_declined, rbac.invitation_revoked, rbac.invitation_expired
Vector storevector_store.created, vector_store.updated, vector_store.deleted
OpenSearchopensearch.index_created, opensearch.index_deleted, opensearch.reindex_executed
Système (tenant_id=0)system.setting_changed, system.admin_promoted, system.admin_revoked, system.user_created, system.user_password_reset, system.api_key_created, system.api_key_revoked
Files (tenant_id=0)system.queue_task_retried, system.queue_task_deleted, system.queue_task_run_now, system.queue_task_cancelled, system.queue_archived_purged
Basekb.created, kb.updated, kb.deleted, kb.duplicated, kb.clone_started, kb.clone_completed, kb.clone_failed, kb.share_added, kb.share_permission_changed, kb.share_removed
Connaissanceknowledge.created, knowledge.updated, knowledge.deleted, knowledge.batch_deleted, knowledge.reparse_started, knowledge.parse_canceled, knowledge.move_started, knowledge.move_completed, knowledge.move_failed
Tags / datasourcetag.created, tag.updated, tag.deleted, datasource.created, datasource.updated, datasource.deleted, datasource.sync_started, datasource.sync_completed, datasource.sync_failed, datasource.paused, datasource.resumed
Wiki / FAQwiki.content_changed, faq.import_started, faq.import_completed, faq.import_failed

Écriture (service + middleware) ​

  • auditLogService.Log : défaut outcome=success ; échec écriture = ERROR log seulement, jamais d’interruption métier.
  • LogDenied : dédoublonnage 1 min glissant (tenant, actor, rbac.access_denied, template route) via denyDedupWindow ; template route (pas URL brute). Log stderr [rbac] role insufficient à chaque refus.
  • AuditServiceProvider dans gin context ; nil-safe (mode Lite sans audit).

API lecture (internal/handler/audit_log.go) ​

RoutePermissionDescription
GET /api/v1/tenants/:id/audit-logPathTenantMatch + AdminFlux espace ; UnscopedOnly (scope_type='')
GET /api/v1/knowledge-bases/:id/activityCréateur KB ou Admin espace, espace owner (pas consommateur org partagée)Projection activité KB
GET /api/v1/system/admin/audit-logSystemAdmin (+ clé system.audit_read)Événements tenant_id=0

Params : after_id, limit 1–100 (défaut 50, max 100), filtres action / outcome / actor. Réponse next_cursor (min id page, 0 = fin).

Rétention (internal/application/service/audit_log_retention.go) ​

  • audit.retention_days / WEKNORA_AUDIT_RETENTION_DAYS ; défaut 90 jours si section audit: absente ; 0 = pas de purge (archivage externe) ; négatif = erreur validation.
  • AuditLogRetentionRunner : ticker goroutine, délai démarrage 10 min, puis 24h Purge → DeleteOlderThan (DELETE indexé, timeout 30s). Enregistré dans internal/container/container.go avec arrêt gracieux.

Limitation de débit (internal/ratelimit + middleware) ​

Limiteur fenêtre glissante (internal/ratelimit/limiter.go) ​

  • Redis prioritaire : Lua ZSET atomique ; member <instanceID>:<ms>.
  • Fallback localLimiter si Redis indisponible / Lite sans Redis.
  • max par appel Allow ; usages : embed web (minute + 24h par channel+IP), IM (internal/im/service.go).

Auth publique par IP (internal/middleware/auth_public_ratelimit.go) ​

PublicAuthRateLimit() : invitations lookup/register-by-invite, 30/min/IP partagé, 429. Local only ; commentaire : Redis internal/ratelimit pour scale horizontal.

Statistiques de référence modèle (internal/application/repository/model_usage.go) ​

Requêtes usage-by-reference (« quelles ressources utilisent ce modèle ») pour garde-fou suppression, pas facturation tokens :

  • scopeKnowledgeBasesByModelID : champs embedding, summary, image, vlm, asr, wiki, auto_tag… (Postgres ->>, SQLite json_extract).
  • scopeCustomAgentsByModelID : custom_agents.config model/rerank/vlm/asr/query_understand/suggestions…
  • Mémoire long terme : memory_config extract + embedding dans long_term_memory.bindings.
  • Consommateurs : ListModelUsages, limite 50 ; garde suppression dans model.go avec détails HTTP 400.

Usage tokens : Langfuse Generation usage_details (TokenUsage), agrégation UI par modèle / tenant:<id> / session.

Vue d’ensemble des flux d’observabilité ​

Référence d’implémentation ​

Chemins relatifs à la racine du dépôt :

CapacitéChemin source
Logs applicationinternal/logger/logger.go
Debug appels LLMinternal/logger/llm_logger.go
Logs requête / RequestIDinternal/middleware/logger.go
Traces Langfuse (OTel)internal/tracing/langfuse/
Porteur trace inter-processusinternal/types/tracing.go
Audit handler / service / repointernal/handler/audit_log.go, internal/application/service/audit_log.go, internal/application/repository/audit_log.go
Rétention auditinternal/application/service/audit_log_retention.go, internal/config/config.go (applyAuditDefaults)
Actions / modèles auditinternal/types/audit_log.go
Rate limitinternal/ratelimit/limiter.go, internal/middleware/auth_public_ratelimit.go
Health checkinternal/router/router.go (GET /health)
Références modèleinternal/application/repository/model_usage.go

Documentation issue de WeKnora v0.9.0 · MIT License