|
| 1 | +#!/usr/bin/env bash |
| 2 | +# emit-span.sh — PostToolUse hook |
| 3 | +# Transforms Claude Code tool calls into OTLP spans and POSTs to the collector. |
| 4 | +# Silently no-ops when collector is unreachable. Never blocks the agent. |
| 5 | + |
| 6 | +set -euo pipefail |
| 7 | + |
| 8 | +COLLECTOR_URL="${QYL_COLLECTOR_URL:-http://localhost:5100}/v1/traces" |
| 9 | + |
| 10 | +INPUT=$(cat) |
| 11 | + |
| 12 | +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"') |
| 13 | +TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"') |
| 14 | +TOOL_USE_ID=$(echo "$INPUT" | jq -r '.tool_use_id // "unknown"') |
| 15 | +CWD=$(echo "$INPUT" | jq -r '.cwd // ""') |
| 16 | +AGENT_NAME=$(echo "$INPUT" | jq -r '.agent_name // ""') |
| 17 | +AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // ""') |
| 18 | + |
| 19 | +# Derive traceId deterministically from session_id (one trace per session) |
| 20 | +TRACE_ID=$(printf '%s' "$SESSION_ID" | md5 -q 2>/dev/null \ |
| 21 | + || printf '%s' "$SESSION_ID" | md5sum | cut -c1-32) |
| 22 | +TRACE_ID="${TRACE_ID:0:32}" |
| 23 | + |
| 24 | +# Derive spanId from tool_use_id (unique per tool call) |
| 25 | +SPAN_ID=$(printf '%s' "$TOOL_USE_ID" | md5 -q 2>/dev/null \ |
| 26 | + || printf '%s' "$TOOL_USE_ID" | md5sum | cut -c1-16) |
| 27 | +SPAN_ID="${SPAN_ID:0:16}" |
| 28 | + |
| 29 | +NOW_NS=$(python3 -c "import time; print(int(time.time() * 1e9))" 2>/dev/null \ |
| 30 | + || date +%s000000000) |
| 31 | + |
| 32 | +TOOL_ATTRS=$(echo "$INPUT" | jq -c '[ |
| 33 | + if .tool_input.file_path then { key: "file.path", value: { stringValue: .tool_input.file_path } } else empty end, |
| 34 | + if .tool_input.command then { key: "bash.command", value: { stringValue: (.tool_input.command | .[0:500]) } } else empty end, |
| 35 | + if .tool_input.pattern then { key: "search.pattern", value: { stringValue: .tool_input.pattern } } else empty end, |
| 36 | + if .tool_input.query then { key: "search.query", value: { stringValue: .tool_input.query } } else empty end, |
| 37 | + if .tool_input.url then { key: "http.url", value: { stringValue: .tool_input.url } } else empty end, |
| 38 | + if .tool_input.content then { key: "file.size_bytes", value: { intValue: (.tool_input.content | length | tostring) } } else empty end, |
| 39 | + if .tool_input.prompt then { key: "task.prompt", value: { stringValue: (.tool_input.prompt | .[0:200]) } } else empty end, |
| 40 | + if .tool_input.subagent_type then { key: "task.subagent_type", value: { stringValue: .tool_input.subagent_type } } else empty end |
| 41 | +]') |
| 42 | + |
| 43 | +AGENT_ATTRS=$(jq -cn \ |
| 44 | + --arg name "$AGENT_NAME" --arg type "$AGENT_TYPE" \ |
| 45 | + '[ |
| 46 | + if $name != "" then { key: "agent.name", value: { stringValue: $name } } else empty end, |
| 47 | + if $type != "" then { key: "agent.type", value: { stringValue: $type } } else empty end |
| 48 | + ]') |
| 49 | + |
| 50 | +ALL_ATTRS=$(jq -cn \ |
| 51 | + --arg tool "$TOOL_NAME" \ |
| 52 | + --argjson tool_attrs "$TOOL_ATTRS" \ |
| 53 | + --argjson agent_attrs "$AGENT_ATTRS" \ |
| 54 | + '[{ key: "tool.name", value: { stringValue: $tool } }] + $tool_attrs + $agent_attrs') |
| 55 | + |
| 56 | +OTLP_PAYLOAD=$(jq -n \ |
| 57 | + --arg trace_id "$TRACE_ID" --arg span_id "$SPAN_ID" \ |
| 58 | + --arg tool "$TOOL_NAME" --arg session "$SESSION_ID" --arg cwd "$CWD" \ |
| 59 | + --arg now_ns "$NOW_NS" --argjson attrs "$ALL_ATTRS" \ |
| 60 | + '{ |
| 61 | + resourceSpans: [{ |
| 62 | + resource: { attributes: [ |
| 63 | + { key: "service.name", value: { stringValue: "claude-code" } }, |
| 64 | + { key: "session.id", value: { stringValue: $session } }, |
| 65 | + { key: "process.cwd", value: { stringValue: $cwd } } |
| 66 | + ]}, |
| 67 | + scopeSpans: [{ |
| 68 | + scope: { name: "claude-code.hooks", version: "1.0.0" }, |
| 69 | + spans: [{ |
| 70 | + traceId: $trace_id, spanId: $span_id, |
| 71 | + name: ("tool/" + $tool), kind: 3, |
| 72 | + startTimeUnixNano: $now_ns, endTimeUnixNano: $now_ns, |
| 73 | + attributes: $attrs, status: { code: 1 } |
| 74 | + }] |
| 75 | + }] |
| 76 | + }] |
| 77 | + }') |
| 78 | + |
| 79 | +curl -s -X POST "$COLLECTOR_URL" \ |
| 80 | + -H "Content-Type: application/json" \ |
| 81 | + -d "$OTLP_PAYLOAD" \ |
| 82 | + --max-time 2 > /dev/null 2>&1 || true |
0 commit comments