promptlayer 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/claude-agents.d.mts +20 -0
- package/dist/claude-agents.d.ts +20 -0
- package/dist/claude-agents.js +2 -0
- package/dist/claude-agents.js.map +1 -0
- package/dist/esm/{chunk-SWBNW72U.js → chunk-DFBRFJOL.js} +2 -2
- package/dist/esm/{chunk-SWBNW72U.js.map → chunk-DFBRFJOL.js.map} +1 -1
- package/dist/esm/claude-agents.js +2 -0
- package/dist/esm/claude-agents.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/openai-agents.js +2 -2
- package/dist/esm/openai-agents.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/openai-agents.js +2 -2
- package/dist/openai-agents.js.map +1 -1
- package/package.json +18 -1
- package/vendor/claude-agents/trace/.claude-plugin/plugin.json +8 -0
- package/vendor/claude-agents/trace/hooks/hook_utils.py +38 -0
- package/vendor/claude-agents/trace/hooks/hooks.json +60 -0
- package/vendor/claude-agents/trace/hooks/lib.sh +577 -0
- package/vendor/claude-agents/trace/hooks/parse_stop_transcript.py +375 -0
- package/vendor/claude-agents/trace/hooks/post_tool_use.sh +41 -0
- package/vendor/claude-agents/trace/hooks/session_end.sh +37 -0
- package/vendor/claude-agents/trace/hooks/session_start.sh +57 -0
- package/vendor/claude-agents/trace/hooks/stop_hook.sh +123 -0
- package/vendor/claude-agents/trace/hooks/user_prompt_submit.sh +25 -0
- package/vendor/claude-agents/vendor_metadata.json +5 -0
- package/.github/CODEOWNERS +0 -1
- package/.github/workflows/node.js.yml +0 -30
- package/.github/workflows/npm-publish.yml +0 -35
- package/src/groups.ts +0 -16
- package/src/index.ts +0 -383
- package/src/integrations/openai-agents/helpers.test.ts +0 -254
- package/src/integrations/openai-agents/ids.ts +0 -27
- package/src/integrations/openai-agents/index.ts +0 -8
- package/src/integrations/openai-agents/instrumentation.test.ts +0 -46
- package/src/integrations/openai-agents/instrumentation.ts +0 -47
- package/src/integrations/openai-agents/mapping.ts +0 -714
- package/src/integrations/openai-agents/otlp-json.ts +0 -120
- package/src/integrations/openai-agents/processor.test.ts +0 -509
- package/src/integrations/openai-agents/processor.ts +0 -388
- package/src/integrations/openai-agents/time.ts +0 -56
- package/src/integrations/openai-agents/types.ts +0 -49
- package/src/integrations/openai-agents/url.ts +0 -9
- package/src/openai-agents.ts +0 -1
- package/src/promptlayer.ts +0 -125
- package/src/run-error-tracking.test.ts +0 -146
- package/src/span-exporter.ts +0 -120
- package/src/span-wrapper.ts +0 -51
- package/src/templates.ts +0 -37
- package/src/tracing.ts +0 -20
- package/src/track.ts +0 -84
- package/src/types.ts +0 -689
- package/src/utils/blueprint-builder.test.ts +0 -727
- package/src/utils/blueprint-builder.ts +0 -1453
- package/src/utils/errors.test.ts +0 -68
- package/src/utils/errors.ts +0 -62
- package/src/utils/streaming.test.ts +0 -498
- package/src/utils/streaming.ts +0 -1402
- package/src/utils/utils.ts +0 -1228
- package/tsconfig.json +0 -115
- package/tsup.config.ts +0 -20
- package/vitest.config.ts +0 -9
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared runtime helpers for trace plugin hooks.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
umask 077
|
|
6
|
+
|
|
7
|
+
export PL_LOG_FILE="$HOME/.claude/state/promptlayer_hook.log"
|
|
8
|
+
export PL_QUEUE_FILE="$HOME/.claude/state/promptlayer_otlp_queue.ndjson"
|
|
9
|
+
export PL_SESSION_STATE_DIR="$HOME/.claude/state/promptlayer_sessions"
|
|
10
|
+
export PL_LOCK_DIR="$HOME/.claude/state/promptlayer_locks"
|
|
11
|
+
|
|
12
|
+
export PL_DEBUG="${PROMPTLAYER_CC_DEBUG:-false}"
|
|
13
|
+
export PL_API_KEY="${PROMPTLAYER_API_KEY:-}"
|
|
14
|
+
export PL_OTLP_ENDPOINT="${PROMPTLAYER_OTLP_ENDPOINT:-https://api.promptlayer.com/v1/traces}"
|
|
15
|
+
export PL_QUEUE_DRAIN_LIMIT="${PROMPTLAYER_QUEUE_DRAIN_LIMIT:-10}"
|
|
16
|
+
export PL_OTLP_CONNECT_TIMEOUT="${PROMPTLAYER_OTLP_CONNECT_TIMEOUT:-5}"
|
|
17
|
+
export PL_OTLP_MAX_TIME="${PROMPTLAYER_OTLP_MAX_TIME:-12}"
|
|
18
|
+
export PL_PLUGIN_VERSION="1.0.0"
|
|
19
|
+
PL_CC_VERSION="$(claude --version 2>/dev/null || echo 'unknown')"
|
|
20
|
+
export PL_CC_VERSION
|
|
21
|
+
export PL_USER_AGENT="promptlayer-claude-plugin/${PL_PLUGIN_VERSION} claude-code/${PL_CC_VERSION}"
|
|
22
|
+
PL_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
export PL_HOOKS_DIR
|
|
24
|
+
|
|
25
|
+
mkdir -p "$(dirname "$PL_LOG_FILE")"
|
|
26
|
+
mkdir -p "$PL_SESSION_STATE_DIR"
|
|
27
|
+
mkdir -p "$PL_LOCK_DIR"
|
|
28
|
+
chmod 700 "$(dirname "$PL_LOG_FILE")" "$PL_SESSION_STATE_DIR" 2>/dev/null || true
|
|
29
|
+
chmod 700 "$PL_LOCK_DIR" 2>/dev/null || true
|
|
30
|
+
|
|
31
|
+
log() {
|
|
32
|
+
printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >>"$PL_LOG_FILE"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
debug() {
|
|
36
|
+
local v
|
|
37
|
+
v="$(echo "$PL_DEBUG" | tr '[:upper:]' '[:lower:]')"
|
|
38
|
+
if [[ "$v" == "1" || "$v" == "true" || "$v" == "yes" || "$v" == "on" ]]; then
|
|
39
|
+
log "DEBUG" "$1"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
tracing_enabled() {
|
|
44
|
+
local v
|
|
45
|
+
v="$(echo "${TRACE_TO_PROMPTLAYER:-true}" | tr '[:upper:]' '[:lower:]')"
|
|
46
|
+
[[ "$v" == "1" || "$v" == "true" || "$v" == "yes" || "$v" == "on" ]]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
check_requirements() {
|
|
50
|
+
local cmd
|
|
51
|
+
for cmd in jq curl uuidgen python3; do
|
|
52
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
53
|
+
log "ERROR" "Missing required command: $cmd"
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
if [[ -z "$PL_API_KEY" ]]; then
|
|
58
|
+
log "ERROR" "PROMPTLAYER_API_KEY is not set"
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
generate_trace_id() {
|
|
65
|
+
uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
generate_span_id() {
|
|
69
|
+
uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]' | cut -c1-16
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
parse_traceparent() {
|
|
73
|
+
local raw="${1:-}"
|
|
74
|
+
[[ -z "$raw" ]] && return 1
|
|
75
|
+
|
|
76
|
+
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
77
|
+
|
|
78
|
+
if [[ ! "$raw" =~ ^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.+)?$ ]]; then
|
|
79
|
+
return 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
local version="${BASH_REMATCH[1]}"
|
|
83
|
+
local trace_id="${BASH_REMATCH[2]}"
|
|
84
|
+
local parent_span_id="${BASH_REMATCH[3]}"
|
|
85
|
+
local trace_flags="${BASH_REMATCH[4]}"
|
|
86
|
+
local suffix="${BASH_REMATCH[5]:-}"
|
|
87
|
+
|
|
88
|
+
if [[ "$version" == "ff" ]]; then
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
if [[ "$version" == "00" && -n "$suffix" ]]; then
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
if [[ "$trace_id" == "00000000000000000000000000000000" ]]; then
|
|
95
|
+
return 1
|
|
96
|
+
fi
|
|
97
|
+
if [[ "$parent_span_id" == "0000000000000000" ]]; then
|
|
98
|
+
return 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
printf '%s %s %s %s\n' "$version" "$trace_id" "$parent_span_id" "$trace_flags"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
load_initial_trace_context() {
|
|
105
|
+
PL_INITIAL_TRACEPARENT_VERSION=""
|
|
106
|
+
PL_INITIAL_TRACE_ID=""
|
|
107
|
+
PL_INITIAL_PARENT_SPAN_ID=""
|
|
108
|
+
PL_INITIAL_TRACE_FLAGS=""
|
|
109
|
+
PL_INITIAL_TRACE_CONTEXT_SOURCE="generated"
|
|
110
|
+
|
|
111
|
+
local raw="${PROMPTLAYER_TRACEPARENT:-}"
|
|
112
|
+
if [[ -z "$raw" ]]; then
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
local parsed
|
|
117
|
+
if ! parsed="$(parse_traceparent "$raw")"; then
|
|
118
|
+
log "WARN" "Ignoring invalid PROMPTLAYER_TRACEPARENT"
|
|
119
|
+
return 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
read -r PL_INITIAL_TRACEPARENT_VERSION PL_INITIAL_TRACE_ID PL_INITIAL_PARENT_SPAN_ID PL_INITIAL_TRACE_FLAGS <<<"$parsed"
|
|
123
|
+
PL_INITIAL_TRACE_CONTEXT_SOURCE="external_traceparent"
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
normalize_hex_id() {
|
|
128
|
+
local raw="$1"
|
|
129
|
+
local expected_len="$2"
|
|
130
|
+
local fallback="$3"
|
|
131
|
+
local label="$4"
|
|
132
|
+
|
|
133
|
+
local clean
|
|
134
|
+
clean="$(echo "$raw" | tr -cd '[:xdigit:]' | tr '[:upper:]' '[:lower:]')"
|
|
135
|
+
if [[ -z "$clean" ]]; then
|
|
136
|
+
clean="$fallback"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if ((${#clean} > expected_len)); then
|
|
140
|
+
clean="${clean:0:expected_len}"
|
|
141
|
+
elif ((${#clean} < expected_len)); then
|
|
142
|
+
clean="$(printf "%-${expected_len}s" "$clean" | tr ' ' '0')"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [[ "$clean" != "$raw" ]]; then
|
|
146
|
+
log "WARN" "Normalized $label from '$raw' to '$clean'"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
echo "$clean"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
hex_to_base64() {
|
|
153
|
+
local hex="$1"
|
|
154
|
+
python3 "$PL_HOOKS_DIR/hook_utils.py" hex_to_base64 "$hex"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
now_ns() {
|
|
158
|
+
python3 "$PL_HOOKS_DIR/hook_utils.py" now_ns
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
session_state_file() {
|
|
162
|
+
echo "$PL_SESSION_STATE_DIR/$1.json"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
acquire_session_lock() {
|
|
166
|
+
local sid="$1"
|
|
167
|
+
[[ -z "$sid" ]] && return 1
|
|
168
|
+
|
|
169
|
+
local lock_dir="$PL_LOCK_DIR/$sid.lock"
|
|
170
|
+
local attempts=0
|
|
171
|
+
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
172
|
+
attempts=$((attempts + 1))
|
|
173
|
+
if ((attempts >= 250)); then
|
|
174
|
+
log "ERROR" "Timed out waiting for session lock session_id=$sid"
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
sleep 0.02
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
export PL_HELD_SESSION_LOCK="$lock_dir"
|
|
181
|
+
return 0
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
release_session_lock() {
|
|
185
|
+
local lock_dir="${PL_HELD_SESSION_LOCK:-}"
|
|
186
|
+
if [[ -n "$lock_dir" ]]; then
|
|
187
|
+
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir"
|
|
188
|
+
unset PL_HELD_SESSION_LOCK
|
|
189
|
+
fi
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
acquire_queue_lock() {
|
|
194
|
+
local lock_dir="$PL_LOCK_DIR/queue.lock"
|
|
195
|
+
local attempts=0
|
|
196
|
+
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
197
|
+
attempts=$((attempts + 1))
|
|
198
|
+
if ((attempts >= 250)); then
|
|
199
|
+
log "ERROR" "Timed out waiting for queue lock"
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
sleep 0.02
|
|
203
|
+
done
|
|
204
|
+
|
|
205
|
+
export PL_HELD_QUEUE_LOCK="$lock_dir"
|
|
206
|
+
return 0
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
release_queue_lock() {
|
|
210
|
+
local lock_dir="${PL_HELD_QUEUE_LOCK:-}"
|
|
211
|
+
if [[ -n "$lock_dir" ]]; then
|
|
212
|
+
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir"
|
|
213
|
+
unset PL_HELD_QUEUE_LOCK
|
|
214
|
+
fi
|
|
215
|
+
return 0
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get_session_state() {
|
|
219
|
+
local sid="$1"
|
|
220
|
+
local key="$2"
|
|
221
|
+
local f
|
|
222
|
+
f="$(session_state_file "$sid")"
|
|
223
|
+
if [[ -f "$f" ]]; then
|
|
224
|
+
jq -r ".${key} // empty" "$f" 2>/dev/null || true
|
|
225
|
+
fi
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
set_session_state() {
|
|
229
|
+
local sid="$1"
|
|
230
|
+
local key="$2"
|
|
231
|
+
local val="$3"
|
|
232
|
+
local f
|
|
233
|
+
f="$(session_state_file "$sid")"
|
|
234
|
+
local current
|
|
235
|
+
current='{}'
|
|
236
|
+
if [[ -f "$f" ]]; then
|
|
237
|
+
current="$(cat "$f")"
|
|
238
|
+
fi
|
|
239
|
+
echo "$current" | jq --arg k "$key" --arg v "$val" '.[$k] = $v' >"$f"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
ensure_session_initialized() {
|
|
243
|
+
local sid="$1"
|
|
244
|
+
local requested_start_ns="${2:-}"
|
|
245
|
+
[[ -z "$sid" ]] && return 1
|
|
246
|
+
|
|
247
|
+
local trace_id session_span_id session_parent_span_id session_start_ns init_source pending_tool_calls
|
|
248
|
+
trace_id="$(get_session_state "$sid" trace_id)"
|
|
249
|
+
session_span_id="$(get_session_state "$sid" session_span_id)"
|
|
250
|
+
session_parent_span_id="$(get_session_state "$sid" session_parent_span_id)"
|
|
251
|
+
session_start_ns="$(get_session_state "$sid" session_start_ns)"
|
|
252
|
+
init_source="$(get_session_state "$sid" session_init_source)"
|
|
253
|
+
pending_tool_calls="$(get_session_state "$sid" pending_tool_calls)"
|
|
254
|
+
|
|
255
|
+
# Normal path: SessionStart already created state.
|
|
256
|
+
if [[ -n "$trace_id" && -n "$session_span_id" ]]; then
|
|
257
|
+
if [[ -z "$session_start_ns" ]]; then
|
|
258
|
+
[[ -z "$requested_start_ns" ]] && requested_start_ns="$(now_ns)"
|
|
259
|
+
set_session_state "$sid" session_start_ns "$requested_start_ns"
|
|
260
|
+
fi
|
|
261
|
+
if [[ -z "$init_source" ]]; then
|
|
262
|
+
set_session_state "$sid" session_init_source "unknown"
|
|
263
|
+
fi
|
|
264
|
+
if [[ -z "$pending_tool_calls" ]]; then
|
|
265
|
+
set_session_state "$sid" pending_tool_calls "[]"
|
|
266
|
+
fi
|
|
267
|
+
if [[ -z "$session_parent_span_id" ]]; then
|
|
268
|
+
set_session_state "$sid" session_parent_span_id ""
|
|
269
|
+
fi
|
|
270
|
+
if [[ -z "$(get_session_state "$sid" session_traceparent_version)" ]]; then
|
|
271
|
+
set_session_state "$sid" session_traceparent_version ""
|
|
272
|
+
fi
|
|
273
|
+
if [[ -z "$(get_session_state "$sid" session_trace_flags)" ]]; then
|
|
274
|
+
set_session_state "$sid" session_trace_flags ""
|
|
275
|
+
fi
|
|
276
|
+
if [[ -z "$(get_session_state "$sid" trace_context_source)" ]]; then
|
|
277
|
+
set_session_state "$sid" trace_context_source "generated"
|
|
278
|
+
fi
|
|
279
|
+
return 0
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Fallback path for SDK environments that do not surface SessionStart.
|
|
283
|
+
[[ -z "$requested_start_ns" ]] && requested_start_ns="$(now_ns)"
|
|
284
|
+
load_initial_trace_context || true
|
|
285
|
+
[[ -z "$trace_id" ]] && trace_id="${PL_INITIAL_TRACE_ID:-}"
|
|
286
|
+
[[ -z "$trace_id" ]] && trace_id="$(generate_trace_id)"
|
|
287
|
+
[[ -z "$session_span_id" ]] && session_span_id="$(generate_span_id)"
|
|
288
|
+
|
|
289
|
+
set_session_state "$sid" trace_id "$trace_id"
|
|
290
|
+
set_session_state "$sid" session_span_id "$session_span_id"
|
|
291
|
+
set_session_state "$sid" session_parent_span_id "${PL_INITIAL_PARENT_SPAN_ID:-}"
|
|
292
|
+
set_session_state "$sid" session_start_ns "$requested_start_ns"
|
|
293
|
+
set_session_state "$sid" current_turn_start_ns ""
|
|
294
|
+
set_session_state "$sid" pending_tool_calls "[]"
|
|
295
|
+
set_session_state "$sid" session_init_source "lazy_init"
|
|
296
|
+
set_session_state "$sid" session_traceparent_version "${PL_INITIAL_TRACEPARENT_VERSION:-}"
|
|
297
|
+
set_session_state "$sid" session_trace_flags "${PL_INITIAL_TRACE_FLAGS:-}"
|
|
298
|
+
set_session_state "$sid" trace_context_source "${PL_INITIAL_TRACE_CONTEXT_SOURCE:-generated}"
|
|
299
|
+
|
|
300
|
+
log "INFO" "Session initialized lazily session_id=$sid trace_id=$trace_id"
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
post_otlp_payload_file() {
|
|
304
|
+
local payload_file="$1"
|
|
305
|
+
local status response_file
|
|
306
|
+
response_file="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-response.XXXXXX")"
|
|
307
|
+
status="$(curl -sS -o "$response_file" -w "%{http_code}" -X POST \
|
|
308
|
+
-H "Content-Type: application/json" \
|
|
309
|
+
-H "X-Api-Key: $PL_API_KEY" \
|
|
310
|
+
-H "User-Agent: $PL_USER_AGENT" \
|
|
311
|
+
--connect-timeout "$PL_OTLP_CONNECT_TIMEOUT" \
|
|
312
|
+
--max-time "$PL_OTLP_MAX_TIME" \
|
|
313
|
+
"$PL_OTLP_ENDPOINT" \
|
|
314
|
+
-d @"$payload_file" || true)"
|
|
315
|
+
|
|
316
|
+
if [[ "$status" != "200" ]]; then
|
|
317
|
+
log "ERROR" "Failed OTLP export status=$status"
|
|
318
|
+
rm -f "$response_file"
|
|
319
|
+
return 1
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
# OTLP JSON can return 200 with partialSuccess and rejected spans.
|
|
323
|
+
# Treat this as non-retryable (same payload is likely to be rejected again).
|
|
324
|
+
if command -v jq >/dev/null 2>&1; then
|
|
325
|
+
local rejected
|
|
326
|
+
rejected="$(jq -r '.partialSuccess.rejectedSpans // 0' "$response_file" 2>/dev/null || echo 0)"
|
|
327
|
+
if [[ "$rejected" != "0" ]]; then
|
|
328
|
+
local message
|
|
329
|
+
message="$(jq -r '.partialSuccess.errorMessage // "Unknown rejection"' "$response_file" 2>/dev/null || true)"
|
|
330
|
+
log "ERROR" "OTLP partial success: rejected_spans=$rejected message=$message"
|
|
331
|
+
rm -f "$response_file"
|
|
332
|
+
return 2
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
rm -f "$response_file"
|
|
337
|
+
return 0
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
append_to_otlp_queue_file() {
|
|
341
|
+
local payload_file="$1"
|
|
342
|
+
acquire_queue_lock || return 1
|
|
343
|
+
# Compact to single line to preserve ndjson format
|
|
344
|
+
jq -c '.' "$payload_file" >>"$PL_QUEUE_FILE"
|
|
345
|
+
chmod 600 "$PL_QUEUE_FILE" 2>/dev/null || true
|
|
346
|
+
release_queue_lock
|
|
347
|
+
return 0
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
drain_otlp_queue() {
|
|
351
|
+
if [[ ! -s "$PL_QUEUE_FILE" ]]; then
|
|
352
|
+
return 0
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
local drain_limit="$PL_QUEUE_DRAIN_LIMIT"
|
|
356
|
+
if [[ ! "$drain_limit" =~ ^[0-9]+$ ]]; then
|
|
357
|
+
drain_limit=10
|
|
358
|
+
fi
|
|
359
|
+
if ((drain_limit <= 0)); then
|
|
360
|
+
return 0
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
acquire_queue_lock || return 0
|
|
364
|
+
|
|
365
|
+
local -a queue_payloads
|
|
366
|
+
mapfile -t queue_payloads <"$PL_QUEUE_FILE" || true
|
|
367
|
+
|
|
368
|
+
local total max_attempts i rc replayed dropped retryable_fail fail_index
|
|
369
|
+
total="${#queue_payloads[@]}"
|
|
370
|
+
if ((total == 0)); then
|
|
371
|
+
release_queue_lock
|
|
372
|
+
return 0
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
max_attempts=$((total < drain_limit ? total : drain_limit))
|
|
376
|
+
replayed=0
|
|
377
|
+
dropped=0
|
|
378
|
+
retryable_fail=0
|
|
379
|
+
fail_index=-1
|
|
380
|
+
|
|
381
|
+
local queued_tmp
|
|
382
|
+
queued_tmp="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-queued.XXXXXX")"
|
|
383
|
+
|
|
384
|
+
for ((i = 0; i < max_attempts; i++)); do
|
|
385
|
+
if [[ -z "${queue_payloads[$i]}" ]]; then
|
|
386
|
+
continue
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
printf '%s' "${queue_payloads[$i]}" >"$queued_tmp"
|
|
390
|
+
|
|
391
|
+
if post_otlp_payload_file "$queued_tmp"; then
|
|
392
|
+
replayed=$((replayed + 1))
|
|
393
|
+
continue
|
|
394
|
+
else
|
|
395
|
+
rc=$?
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
if [[ "$rc" == "2" ]]; then
|
|
399
|
+
dropped=$((dropped + 1))
|
|
400
|
+
continue
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
retryable_fail=1
|
|
404
|
+
fail_index="$i"
|
|
405
|
+
break
|
|
406
|
+
done
|
|
407
|
+
|
|
408
|
+
rm -f "$queued_tmp"
|
|
409
|
+
|
|
410
|
+
local tmp remaining_start
|
|
411
|
+
tmp="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-queue.XXXXXX")"
|
|
412
|
+
if ((retryable_fail == 1)); then
|
|
413
|
+
remaining_start="$fail_index"
|
|
414
|
+
else
|
|
415
|
+
remaining_start="$max_attempts"
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
for ((i = remaining_start; i < total; i++)); do
|
|
419
|
+
printf '%s\n' "${queue_payloads[$i]}" >>"$tmp"
|
|
420
|
+
done
|
|
421
|
+
|
|
422
|
+
mv "$tmp" "$PL_QUEUE_FILE"
|
|
423
|
+
chmod 600 "$PL_QUEUE_FILE" 2>/dev/null || true
|
|
424
|
+
release_queue_lock
|
|
425
|
+
|
|
426
|
+
if ((replayed > 0)); then
|
|
427
|
+
log "INFO" "Drained queued OTLP payloads count=$replayed"
|
|
428
|
+
fi
|
|
429
|
+
if ((dropped > 0)); then
|
|
430
|
+
log "WARN" "Dropped non-retryable queued OTLP payloads count=$dropped"
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
return 0
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
send_otlp_payload_file() {
|
|
437
|
+
local payload_file="$1"
|
|
438
|
+
local rc
|
|
439
|
+
|
|
440
|
+
drain_otlp_queue || true
|
|
441
|
+
|
|
442
|
+
if post_otlp_payload_file "$payload_file"; then
|
|
443
|
+
return 0
|
|
444
|
+
else
|
|
445
|
+
rc=$?
|
|
446
|
+
fi
|
|
447
|
+
|
|
448
|
+
if [[ "$rc" == "1" ]]; then
|
|
449
|
+
append_to_otlp_queue_file "$payload_file" || log "ERROR" "Failed to append OTLP payload to queue"
|
|
450
|
+
fi
|
|
451
|
+
return 1
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
kind_int_to_string() {
|
|
455
|
+
case "$1" in
|
|
456
|
+
0) echo "SPAN_KIND_UNSPECIFIED" ;;
|
|
457
|
+
1) echo "SPAN_KIND_INTERNAL" ;;
|
|
458
|
+
2) echo "SPAN_KIND_SERVER" ;;
|
|
459
|
+
3) echo "SPAN_KIND_CLIENT" ;;
|
|
460
|
+
4) echo "SPAN_KIND_PRODUCER" ;;
|
|
461
|
+
5) echo "SPAN_KIND_CONSUMER" ;;
|
|
462
|
+
*) echo "SPAN_KIND_UNSPECIFIED" ;;
|
|
463
|
+
esac
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
build_span_json() {
|
|
467
|
+
local trace_id="$1"
|
|
468
|
+
local span_id="$2"
|
|
469
|
+
local parent_span_id="$3"
|
|
470
|
+
local name="$4"
|
|
471
|
+
local kind="$5"
|
|
472
|
+
local start_ns="$6"
|
|
473
|
+
local end_ns="$7"
|
|
474
|
+
local attrs_json="$8"
|
|
475
|
+
|
|
476
|
+
# Convert integer kind to protobuf JSON enum string if needed
|
|
477
|
+
if [[ "$kind" =~ ^[0-9]+$ ]]; then
|
|
478
|
+
kind="$(kind_int_to_string "$kind")"
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
trace_id="$(normalize_hex_id "$trace_id" 32 "$(generate_trace_id)" "trace_id")"
|
|
482
|
+
span_id="$(normalize_hex_id "$span_id" 16 "$(generate_span_id)" "span_id")"
|
|
483
|
+
if [[ -n "$parent_span_id" ]]; then
|
|
484
|
+
parent_span_id="$(normalize_hex_id "$parent_span_id" 16 "$(generate_span_id)" "parent_span_id")"
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
local trace_id_b64 span_id_b64 parent_span_id_b64
|
|
488
|
+
trace_id_b64="$(hex_to_base64 "$trace_id")"
|
|
489
|
+
span_id_b64="$(hex_to_base64 "$span_id")"
|
|
490
|
+
parent_span_id_b64=""
|
|
491
|
+
if [[ -n "$parent_span_id" ]]; then
|
|
492
|
+
parent_span_id_b64="$(hex_to_base64 "$parent_span_id")"
|
|
493
|
+
fi
|
|
494
|
+
|
|
495
|
+
local span_json
|
|
496
|
+
span_json="$(jq -cn \
|
|
497
|
+
--arg trace_id "$trace_id_b64" \
|
|
498
|
+
--arg span_id "$span_id_b64" \
|
|
499
|
+
--arg parent_span_id "$parent_span_id_b64" \
|
|
500
|
+
--arg name "$name" \
|
|
501
|
+
--arg kind "$kind" \
|
|
502
|
+
--arg start "$start_ns" \
|
|
503
|
+
--arg end "$end_ns" \
|
|
504
|
+
--argjson attributes "$attrs_json" \
|
|
505
|
+
'{
|
|
506
|
+
traceId: $trace_id,
|
|
507
|
+
spanId: $span_id,
|
|
508
|
+
parentSpanId: (if $parent_span_id == "" then null else $parent_span_id end),
|
|
509
|
+
name: $name,
|
|
510
|
+
kind: $kind,
|
|
511
|
+
startTimeUnixNano: $start,
|
|
512
|
+
endTimeUnixNano: $end,
|
|
513
|
+
attributes: (
|
|
514
|
+
$attributes
|
|
515
|
+
| to_entries
|
|
516
|
+
| map(select(.value != null))
|
|
517
|
+
| map(
|
|
518
|
+
. as $kv
|
|
519
|
+
| {
|
|
520
|
+
key: $kv.key,
|
|
521
|
+
value: (
|
|
522
|
+
if ($kv.value | type) == "string" then
|
|
523
|
+
{stringValue: $kv.value}
|
|
524
|
+
elif ($kv.value | type) == "boolean" then
|
|
525
|
+
{boolValue: $kv.value}
|
|
526
|
+
elif ($kv.value | type) == "number" then
|
|
527
|
+
if ($kv.value | floor) == $kv.value then
|
|
528
|
+
{intValue: ($kv.value | tostring)}
|
|
529
|
+
else
|
|
530
|
+
{doubleValue: $kv.value}
|
|
531
|
+
end
|
|
532
|
+
else
|
|
533
|
+
{stringValue: ($kv.value | tojson)}
|
|
534
|
+
end
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
}')"
|
|
540
|
+
|
|
541
|
+
echo "$span_json"
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
emit_spans_batch_file() {
|
|
545
|
+
local spans_file="$1"
|
|
546
|
+
if [[ ! -s "$spans_file" ]]; then
|
|
547
|
+
return 0
|
|
548
|
+
fi
|
|
549
|
+
|
|
550
|
+
local payload_file
|
|
551
|
+
payload_file="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-batch.XXXXXX")"
|
|
552
|
+
jq -cs '{resourceSpans:[{resource:{attributes:[{key:"service.name",value:{stringValue:"claude-code"}}]},scopeSpans:[{spans:.}]}]}' "$spans_file" >"$payload_file"
|
|
553
|
+
send_otlp_payload_file "$payload_file"
|
|
554
|
+
local rc=$?
|
|
555
|
+
rm -f "$payload_file"
|
|
556
|
+
return $rc
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
emit_span() {
|
|
560
|
+
local trace_id="$1"
|
|
561
|
+
local span_id="$2"
|
|
562
|
+
local parent_span_id="$3"
|
|
563
|
+
local name="$4"
|
|
564
|
+
local kind="$5"
|
|
565
|
+
local start_ns="$6"
|
|
566
|
+
local end_ns="$7"
|
|
567
|
+
local attrs_json="$8"
|
|
568
|
+
|
|
569
|
+
local span_json spans_file
|
|
570
|
+
span_json="$(build_span_json "$trace_id" "$span_id" "$parent_span_id" "$name" "$kind" "$start_ns" "$end_ns" "$attrs_json")" || return 1
|
|
571
|
+
spans_file="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-span.XXXXXX")"
|
|
572
|
+
printf '%s\n' "$span_json" >"$spans_file"
|
|
573
|
+
emit_spans_batch_file "$spans_file"
|
|
574
|
+
local rc=$?
|
|
575
|
+
rm -f "$spans_file"
|
|
576
|
+
return $rc
|
|
577
|
+
}
|