feat(observability): enrich request logs with auth and knowledge context

- Collect per-request auth diagnostics (header presence, scheme, token
  fingerprint, failure stage/reason, subject, expiry) in the auth
  middleware and expose them through RequestLogContextFromGin so the
  shared logger middleware can emit them.
- Add route, query, user-agent, and referer fields to the request log.
- Log knowledge resolve outcome per stage (no_search_points,
  no_candidate_text, no_active_candidates, rerank_failed_fallback,
  resolved) with candidate/selected snippet previews and precise facts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 17:16:57 +08:00
parent 4bbce5f083
commit 052bf38dc4
4 changed files with 357 additions and 2 deletions
@@ -790,6 +790,7 @@ func (s *KnowledgeService) ResolveContext(
return nil, response.ErrServiceUnavailable(50354, "qdrant_search_failed", err.Error())
}
if len(searchPoints) == 0 {
s.logKnowledgeResolve(baseContext, tenantID, query, "no_search_points", nil, nil)
return baseContext, nil
}
@@ -812,6 +813,7 @@ func (s *KnowledgeService) ResolveContext(
documents = append(documents, text)
}
if len(candidates) == 0 {
s.logKnowledgeResolve(baseContext, tenantID, query, "no_candidate_text", nil, nil)
return baseContext, nil
}
@@ -820,6 +822,7 @@ func (s *KnowledgeService) ResolveContext(
return nil, err
}
if len(candidates) == 0 {
s.logKnowledgeResolve(baseContext, tenantID, query, "no_active_candidates", nil, nil)
return baseContext, nil
}
@@ -834,6 +837,7 @@ func (s *KnowledgeService) ResolveContext(
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
baseContext.Snippets = selected
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, groupIDs, selected)
s.logKnowledgeResolve(baseContext, tenantID, query, "rerank_failed_fallback", candidates, selected)
return baseContext, nil
}
@@ -853,9 +857,97 @@ func (s *KnowledgeService) ResolveContext(
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
baseContext.Snippets = selected
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, groupIDs, selected)
s.logKnowledgeResolve(baseContext, tenantID, query, "resolved", candidates, selected)
return baseContext, nil
}
func (s *KnowledgeService) logKnowledgeResolve(
knowledgeCtx *KnowledgeContext,
tenantID int64,
query string,
stage string,
candidates []KnowledgeSnippet,
selected []KnowledgeSnippet,
) {
if s == nil || s.logger == nil || knowledgeCtx == nil {
return
}
s.logger.Info("knowledge resolve result",
zap.String("stage", strings.TrimSpace(stage)),
zap.Int64("tenant_id", tenantID),
zap.Int64s("group_ids", knowledgeCtx.GroupIDs),
zap.Strings("group_names", knowledgeCtx.GroupNames),
zap.String("query", knowledgeLogPreview(query, 600)),
zap.Int("candidate_count", len(candidates)),
zap.Int("selected_count", len(selected)),
zap.Int("precise_fact_count", len(knowledgeCtx.PreciseFacts)),
zap.Any("candidates", summarizeKnowledgeSnippetsForLog(candidates)),
zap.Any("selected_snippets", summarizeKnowledgeSnippetsForLog(selected)),
zap.Strings("precise_facts", summarizeKnowledgeFactsForLog(knowledgeCtx.PreciseFacts)),
)
}
func summarizeKnowledgeSnippetsForLog(snippets []KnowledgeSnippet) []map[string]any {
if len(snippets) == 0 {
return []map[string]any{}
}
summary := make([]map[string]any, 0, len(snippets))
for _, snippet := range snippets {
entry := map[string]any{
"point_id": snippet.PointID,
"group_id": snippet.GroupID,
"group_name": strings.TrimSpace(snippet.GroupName),
"item_id": snippet.KnowledgeItemID,
"item_name": strings.TrimSpace(snippet.ItemName),
"score": snippet.Score,
"text": knowledgeLogPreview(snippet.Text, 240),
}
if sourceType := strings.TrimSpace(snippet.SourceType); sourceType != "" {
entry["source_type"] = sourceType
}
if sourceURI := strings.TrimSpace(derefKnowledgeString(snippet.SourceURI)); sourceURI != "" {
entry["source_uri"] = sourceURI
}
if len(snippet.PreciseFacts) > 0 {
entry["precise_facts"] = summarizeKnowledgeFactsForLog(snippet.PreciseFacts)
}
summary = append(summary, entry)
}
return summary
}
func summarizeKnowledgeFactsForLog(facts []string) []string {
if len(facts) == 0 {
return []string{}
}
limit := len(facts)
if limit > 12 {
limit = 12
}
summary := make([]string, 0, limit)
for _, fact := range facts[:limit] {
summary = append(summary, knowledgeLogPreview(fact, 240))
}
return summary
}
func knowledgeLogPreview(value string, maxRunes int) string {
text := strings.TrimSpace(value)
if text == "" {
return ""
}
runes := []rune(text)
if maxRunes <= 0 || len(runes) <= maxRunes {
return text
}
return strings.TrimSpace(string(runes[:maxRunes])) + "..."
}
func (s *KnowledgeService) RenderPromptSection(ctx *KnowledgeContext) string {
if ctx == nil || (len(ctx.Snippets) == 0 && len(ctx.PreciseFacts) == 0) {
return ""