netra-zen 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
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.
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.1.dist-info}/METADATA +992 -992
- netra_zen-1.2.1.dist-info/RECORD +36 -0
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.1.dist-info}/licenses/LICENSE.md +1 -1
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.1.dist-info}/top_level.txt +1 -0
- scripts/__init__.py +1 -1
- scripts/__main__.py +5 -5
- scripts/agent_cli.py +7334 -7334
- scripts/agent_logs.py +327 -327
- scripts/demo_log_collection.py +146 -146
- scripts/embed_release_credentials.py +75 -75
- scripts/test_apex_telemetry_debug.py +221 -221
- scripts/verify_log_transmission.py +140 -140
- shared/README.md +47 -0
- shared/TIMING_FIX_COMPLETE.md +80 -0
- shared/__init__.py +12 -0
- shared/types/__init__.py +21 -0
- shared/types/websocket_closure_codes.py +124 -0
- shared/windows_encoding.py +45 -0
- zen/__init__.py +7 -7
- zen/__main__.py +11 -11
- zen/telemetry/__init__.py +14 -14
- zen/telemetry/apex_telemetry.py +259 -259
- zen/telemetry/embedded_credentials.py +60 -59
- zen/telemetry/manager.py +249 -249
- zen_orchestrator.py +3058 -3058
- netra_zen-1.2.0.dist-info/RECORD +0 -30
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.1.dist-info}/WHEEL +0 -0
- {netra_zen-1.2.0.dist-info → netra_zen-1.2.1.dist-info}/entry_points.txt +0 -0
zen/telemetry/__init__.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
"""Telemetry utilities exposed by the Zen package."""
|
2
|
-
|
3
|
-
from .embedded_credentials import get_embedded_credentials, get_project_id
|
4
|
-
from .manager import TelemetryManager, telemetry_manager
|
5
|
-
from .apex_telemetry import run_apex_with_telemetry, ApexTelemetryWrapper
|
6
|
-
|
7
|
-
__all__ = [
|
8
|
-
"TelemetryManager",
|
9
|
-
"telemetry_manager",
|
10
|
-
"get_embedded_credentials",
|
11
|
-
"get_project_id",
|
12
|
-
"run_apex_with_telemetry",
|
13
|
-
"ApexTelemetryWrapper",
|
14
|
-
]
|
1
|
+
"""Telemetry utilities exposed by the Zen package."""
|
2
|
+
|
3
|
+
from .embedded_credentials import get_embedded_credentials, get_project_id
|
4
|
+
from .manager import TelemetryManager, telemetry_manager
|
5
|
+
from .apex_telemetry import run_apex_with_telemetry, ApexTelemetryWrapper
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"TelemetryManager",
|
9
|
+
"telemetry_manager",
|
10
|
+
"get_embedded_credentials",
|
11
|
+
"get_project_id",
|
12
|
+
"run_apex_with_telemetry",
|
13
|
+
"ApexTelemetryWrapper",
|
14
|
+
]
|
zen/telemetry/apex_telemetry.py
CHANGED
@@ -1,259 +1,259 @@
|
|
1
|
-
"""Telemetry wrapper for apex instance tracking.
|
2
|
-
|
3
|
-
This module provides a lightweight wrapper around agent_cli.py subprocess calls
|
4
|
-
to emit OpenTelemetry spans for apex instances without modifying agent_cli.py.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from __future__ import annotations
|
8
|
-
|
9
|
-
import json
|
10
|
-
import logging
|
11
|
-
import os
|
12
|
-
import subprocess
|
13
|
-
import sys
|
14
|
-
import time
|
15
|
-
from typing import Any, Dict, Optional
|
16
|
-
|
17
|
-
from .manager import telemetry_manager
|
18
|
-
|
19
|
-
logger = logging.getLogger(__name__)
|
20
|
-
|
21
|
-
|
22
|
-
class ApexTelemetryWrapper:
|
23
|
-
"""Wrapper to track apex instance telemetry."""
|
24
|
-
|
25
|
-
def __init__(self):
|
26
|
-
self.start_time: Optional[float] = None
|
27
|
-
self.end_time: Optional[float] = None
|
28
|
-
self.exit_code: Optional[int] = None
|
29
|
-
self.message: Optional[str] = None
|
30
|
-
self.env: str = "staging"
|
31
|
-
self.stdout: str = ""
|
32
|
-
self.stderr: str = ""
|
33
|
-
|
34
|
-
def run_apex_with_telemetry(
|
35
|
-
self,
|
36
|
-
agent_cli_path: str,
|
37
|
-
filtered_argv: list,
|
38
|
-
env: Optional[Dict[str, str]] = None
|
39
|
-
) -> int:
|
40
|
-
"""Run agent_cli.py subprocess and emit telemetry span.
|
41
|
-
|
42
|
-
Args:
|
43
|
-
agent_cli_path: Path to agent_cli.py script
|
44
|
-
filtered_argv: Command-line arguments (without 'zen' and '--apex')
|
45
|
-
env: Environment variables to pass to subprocess
|
46
|
-
|
47
|
-
Returns:
|
48
|
-
Exit code from agent_cli subprocess
|
49
|
-
"""
|
50
|
-
self.start_time = time.time()
|
51
|
-
|
52
|
-
# Extract message from argv for telemetry
|
53
|
-
self.message = self._extract_message(filtered_argv)
|
54
|
-
self.env = self._extract_env(filtered_argv)
|
55
|
-
|
56
|
-
# Build command
|
57
|
-
cmd = [sys.executable, agent_cli_path] + filtered_argv
|
58
|
-
|
59
|
-
try:
|
60
|
-
# Use Popen for real-time streaming while still capturing output for telemetry
|
61
|
-
process = subprocess.Popen(
|
62
|
-
cmd,
|
63
|
-
env=env,
|
64
|
-
stdout=subprocess.PIPE,
|
65
|
-
stderr=subprocess.PIPE,
|
66
|
-
text=True,
|
67
|
-
bufsize=1 # Line buffered for real-time output
|
68
|
-
)
|
69
|
-
|
70
|
-
# Collect output while streaming in real-time
|
71
|
-
stdout_lines = []
|
72
|
-
stderr_lines = []
|
73
|
-
|
74
|
-
# Stream stdout in real-time
|
75
|
-
if process.stdout:
|
76
|
-
for line in iter(process.stdout.readline, ''):
|
77
|
-
if line:
|
78
|
-
print(line, end='') # Print immediately for real-time display
|
79
|
-
stdout_lines.append(line)
|
80
|
-
|
81
|
-
# Wait for process to complete and get stderr
|
82
|
-
stderr_output = process.stderr.read() if process.stderr else ""
|
83
|
-
if stderr_output:
|
84
|
-
print(stderr_output, end='', file=sys.stderr)
|
85
|
-
stderr_lines.append(stderr_output)
|
86
|
-
|
87
|
-
# Wait for process to complete
|
88
|
-
self.exit_code = process.wait()
|
89
|
-
|
90
|
-
# Store captured output for telemetry parsing
|
91
|
-
self.stdout = ''.join(stdout_lines)
|
92
|
-
self.stderr = ''.join(stderr_lines)
|
93
|
-
|
94
|
-
except Exception as e:
|
95
|
-
logger.warning(f"Failed to run apex subprocess: {e}")
|
96
|
-
self.exit_code = 1
|
97
|
-
self.stderr = str(e)
|
98
|
-
|
99
|
-
finally:
|
100
|
-
self.end_time = time.time()
|
101
|
-
self._emit_telemetry()
|
102
|
-
|
103
|
-
return self.exit_code or 0
|
104
|
-
|
105
|
-
def _extract_message(self, argv: list) -> str:
|
106
|
-
"""Extract message from command-line arguments."""
|
107
|
-
try:
|
108
|
-
if '--message' in argv:
|
109
|
-
idx = argv.index('--message')
|
110
|
-
if idx + 1 < len(argv):
|
111
|
-
return argv[idx + 1]
|
112
|
-
elif '-m' in argv:
|
113
|
-
idx = argv.index('-m')
|
114
|
-
if idx + 1 < len(argv):
|
115
|
-
return argv[idx + 1]
|
116
|
-
except (ValueError, IndexError):
|
117
|
-
pass
|
118
|
-
return "apex-instance"
|
119
|
-
|
120
|
-
def _extract_env(self, argv: list) -> str:
|
121
|
-
"""Extract environment from command-line arguments."""
|
122
|
-
try:
|
123
|
-
if '--env' in argv:
|
124
|
-
idx = argv.index('--env')
|
125
|
-
if idx + 1 < len(argv):
|
126
|
-
return argv[idx + 1]
|
127
|
-
except (ValueError, IndexError):
|
128
|
-
pass
|
129
|
-
return "staging"
|
130
|
-
|
131
|
-
def _emit_telemetry(self) -> None:
|
132
|
-
"""Emit OpenTelemetry span for apex instance."""
|
133
|
-
if telemetry_manager is None or not hasattr(telemetry_manager, "is_enabled"):
|
134
|
-
logger.debug("Telemetry manager not available")
|
135
|
-
return
|
136
|
-
|
137
|
-
if not telemetry_manager.is_enabled():
|
138
|
-
logger.debug("Telemetry is not enabled")
|
139
|
-
return
|
140
|
-
|
141
|
-
# Calculate duration
|
142
|
-
duration_ms = 0
|
143
|
-
if self.start_time and self.end_time:
|
144
|
-
duration_ms = int((self.end_time - self.start_time) * 1000)
|
145
|
-
|
146
|
-
# Determine status
|
147
|
-
status = "completed" if self.exit_code == 0 else "failed"
|
148
|
-
success = self.exit_code == 0
|
149
|
-
|
150
|
-
# Build attributes for apex.instance span
|
151
|
-
attributes: Dict[str, Any] = {
|
152
|
-
"zen.instance.type": "apex",
|
153
|
-
"zen.instance.name": "apex.instance",
|
154
|
-
"zen.instance.status": status,
|
155
|
-
"zen.instance.success": success,
|
156
|
-
"zen.instance.duration_ms": duration_ms,
|
157
|
-
"zen.instance.exit_code": self.exit_code or 0,
|
158
|
-
"zen.apex.environment": self.env,
|
159
|
-
"zen.apex.message": self._truncate_message(self.message or ""),
|
160
|
-
}
|
161
|
-
|
162
|
-
# Parse JSON output if available (contains token/cost info)
|
163
|
-
json_output = self._parse_json_output()
|
164
|
-
if json_output:
|
165
|
-
self._add_json_metrics(attributes, json_output)
|
166
|
-
|
167
|
-
# Emit span using the telemetry manager's tracer (same way as regular zen instances)
|
168
|
-
try:
|
169
|
-
# Access the tracer the same way telemetry_manager.record_instance_span() does
|
170
|
-
if not hasattr(telemetry_manager, '_tracer') or telemetry_manager._tracer is None:
|
171
|
-
logger.warning("Telemetry manager has no tracer configured")
|
172
|
-
return
|
173
|
-
|
174
|
-
from opentelemetry.trace import SpanKind
|
175
|
-
from google.api_core.exceptions import GoogleAPICallError
|
176
|
-
|
177
|
-
with telemetry_manager._tracer.start_as_current_span(
|
178
|
-
"apex.instance", kind=SpanKind.INTERNAL
|
179
|
-
) as span:
|
180
|
-
for key, value in attributes.items():
|
181
|
-
span.set_attribute(key, value)
|
182
|
-
|
183
|
-
logger.info(f"✅ Emitted apex telemetry span with {len(attributes)} attributes")
|
184
|
-
logger.debug(f"Apex span attributes: {attributes}")
|
185
|
-
|
186
|
-
# Note: Removed force_flush to prevent blocking event streaming
|
187
|
-
# Spans will still be sent via the normal batch export process
|
188
|
-
|
189
|
-
except Exception as exc:
|
190
|
-
logger.error(f"❌ Failed to emit apex telemetry span: {exc}")
|
191
|
-
import traceback
|
192
|
-
logger.debug(f"Traceback: {traceback.format_exc()}")
|
193
|
-
|
194
|
-
def _truncate_message(self, message: str, max_length: int = 200) -> str:
|
195
|
-
"""Truncate message for telemetry attributes."""
|
196
|
-
if len(message) <= max_length:
|
197
|
-
return message
|
198
|
-
return message[:max_length] + "..."
|
199
|
-
|
200
|
-
def _parse_json_output(self) -> Optional[Dict[str, Any]]:
|
201
|
-
"""Parse JSON output from agent_cli stdout if available."""
|
202
|
-
if not self.stdout:
|
203
|
-
return None
|
204
|
-
|
205
|
-
# Try to find JSON in stdout
|
206
|
-
for line in self.stdout.split('\n'):
|
207
|
-
line = line.strip()
|
208
|
-
if line.startswith('{') and line.endswith('}'):
|
209
|
-
try:
|
210
|
-
return json.loads(line)
|
211
|
-
except json.JSONDecodeError:
|
212
|
-
continue
|
213
|
-
|
214
|
-
return None
|
215
|
-
|
216
|
-
def _add_json_metrics(self, attributes: Dict[str, Any], json_output: Dict[str, Any]) -> None:
|
217
|
-
"""Add metrics from JSON output to telemetry attributes."""
|
218
|
-
# Extract token usage if available
|
219
|
-
if 'usage' in json_output:
|
220
|
-
usage = json_output['usage']
|
221
|
-
attributes['zen.tokens.total'] = usage.get('total_tokens', 0)
|
222
|
-
attributes['zen.tokens.input'] = usage.get('input_tokens', 0)
|
223
|
-
attributes['zen.tokens.output'] = usage.get('output_tokens', 0)
|
224
|
-
attributes['zen.tokens.cache.read'] = usage.get('cache_read_tokens', 0)
|
225
|
-
attributes['zen.tokens.cache.creation'] = usage.get('cache_creation_tokens', 0)
|
226
|
-
|
227
|
-
# Extract cost if available
|
228
|
-
if 'cost' in json_output:
|
229
|
-
cost = json_output['cost']
|
230
|
-
if 'total_usd' in cost:
|
231
|
-
attributes['zen.cost.usd_total'] = round(float(cost['total_usd']), 6)
|
232
|
-
|
233
|
-
# Extract run_id if available
|
234
|
-
if 'run_id' in json_output:
|
235
|
-
attributes['zen.apex.run_id'] = json_output['run_id']
|
236
|
-
|
237
|
-
# Extract validation status
|
238
|
-
if 'validation' in json_output:
|
239
|
-
validation = json_output['validation']
|
240
|
-
attributes['zen.apex.validation.passed'] = validation.get('passed', False)
|
241
|
-
|
242
|
-
|
243
|
-
def run_apex_with_telemetry(
|
244
|
-
agent_cli_path: str,
|
245
|
-
filtered_argv: list,
|
246
|
-
env: Optional[Dict[str, str]] = None
|
247
|
-
) -> int:
|
248
|
-
"""Convenience function to run apex with telemetry tracking.
|
249
|
-
|
250
|
-
Args:
|
251
|
-
agent_cli_path: Path to agent_cli.py script
|
252
|
-
filtered_argv: Command-line arguments (without 'zen' and '--apex')
|
253
|
-
env: Environment variables to pass to subprocess
|
254
|
-
|
255
|
-
Returns:
|
256
|
-
Exit code from agent_cli subprocess
|
257
|
-
"""
|
258
|
-
wrapper = ApexTelemetryWrapper()
|
259
|
-
return wrapper.run_apex_with_telemetry(agent_cli_path, filtered_argv, env)
|
1
|
+
"""Telemetry wrapper for apex instance tracking.
|
2
|
+
|
3
|
+
This module provides a lightweight wrapper around agent_cli.py subprocess calls
|
4
|
+
to emit OpenTelemetry spans for apex instances without modifying agent_cli.py.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import json
|
10
|
+
import logging
|
11
|
+
import os
|
12
|
+
import subprocess
|
13
|
+
import sys
|
14
|
+
import time
|
15
|
+
from typing import Any, Dict, Optional
|
16
|
+
|
17
|
+
from .manager import telemetry_manager
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class ApexTelemetryWrapper:
|
23
|
+
"""Wrapper to track apex instance telemetry."""
|
24
|
+
|
25
|
+
def __init__(self):
|
26
|
+
self.start_time: Optional[float] = None
|
27
|
+
self.end_time: Optional[float] = None
|
28
|
+
self.exit_code: Optional[int] = None
|
29
|
+
self.message: Optional[str] = None
|
30
|
+
self.env: str = "staging"
|
31
|
+
self.stdout: str = ""
|
32
|
+
self.stderr: str = ""
|
33
|
+
|
34
|
+
def run_apex_with_telemetry(
|
35
|
+
self,
|
36
|
+
agent_cli_path: str,
|
37
|
+
filtered_argv: list,
|
38
|
+
env: Optional[Dict[str, str]] = None
|
39
|
+
) -> int:
|
40
|
+
"""Run agent_cli.py subprocess and emit telemetry span.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
agent_cli_path: Path to agent_cli.py script
|
44
|
+
filtered_argv: Command-line arguments (without 'zen' and '--apex')
|
45
|
+
env: Environment variables to pass to subprocess
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
Exit code from agent_cli subprocess
|
49
|
+
"""
|
50
|
+
self.start_time = time.time()
|
51
|
+
|
52
|
+
# Extract message from argv for telemetry
|
53
|
+
self.message = self._extract_message(filtered_argv)
|
54
|
+
self.env = self._extract_env(filtered_argv)
|
55
|
+
|
56
|
+
# Build command
|
57
|
+
cmd = [sys.executable, agent_cli_path] + filtered_argv
|
58
|
+
|
59
|
+
try:
|
60
|
+
# Use Popen for real-time streaming while still capturing output for telemetry
|
61
|
+
process = subprocess.Popen(
|
62
|
+
cmd,
|
63
|
+
env=env,
|
64
|
+
stdout=subprocess.PIPE,
|
65
|
+
stderr=subprocess.PIPE,
|
66
|
+
text=True,
|
67
|
+
bufsize=1 # Line buffered for real-time output
|
68
|
+
)
|
69
|
+
|
70
|
+
# Collect output while streaming in real-time
|
71
|
+
stdout_lines = []
|
72
|
+
stderr_lines = []
|
73
|
+
|
74
|
+
# Stream stdout in real-time
|
75
|
+
if process.stdout:
|
76
|
+
for line in iter(process.stdout.readline, ''):
|
77
|
+
if line:
|
78
|
+
print(line, end='') # Print immediately for real-time display
|
79
|
+
stdout_lines.append(line)
|
80
|
+
|
81
|
+
# Wait for process to complete and get stderr
|
82
|
+
stderr_output = process.stderr.read() if process.stderr else ""
|
83
|
+
if stderr_output:
|
84
|
+
print(stderr_output, end='', file=sys.stderr)
|
85
|
+
stderr_lines.append(stderr_output)
|
86
|
+
|
87
|
+
# Wait for process to complete
|
88
|
+
self.exit_code = process.wait()
|
89
|
+
|
90
|
+
# Store captured output for telemetry parsing
|
91
|
+
self.stdout = ''.join(stdout_lines)
|
92
|
+
self.stderr = ''.join(stderr_lines)
|
93
|
+
|
94
|
+
except Exception as e:
|
95
|
+
logger.warning(f"Failed to run apex subprocess: {e}")
|
96
|
+
self.exit_code = 1
|
97
|
+
self.stderr = str(e)
|
98
|
+
|
99
|
+
finally:
|
100
|
+
self.end_time = time.time()
|
101
|
+
self._emit_telemetry()
|
102
|
+
|
103
|
+
return self.exit_code or 0
|
104
|
+
|
105
|
+
def _extract_message(self, argv: list) -> str:
|
106
|
+
"""Extract message from command-line arguments."""
|
107
|
+
try:
|
108
|
+
if '--message' in argv:
|
109
|
+
idx = argv.index('--message')
|
110
|
+
if idx + 1 < len(argv):
|
111
|
+
return argv[idx + 1]
|
112
|
+
elif '-m' in argv:
|
113
|
+
idx = argv.index('-m')
|
114
|
+
if idx + 1 < len(argv):
|
115
|
+
return argv[idx + 1]
|
116
|
+
except (ValueError, IndexError):
|
117
|
+
pass
|
118
|
+
return "apex-instance"
|
119
|
+
|
120
|
+
def _extract_env(self, argv: list) -> str:
|
121
|
+
"""Extract environment from command-line arguments."""
|
122
|
+
try:
|
123
|
+
if '--env' in argv:
|
124
|
+
idx = argv.index('--env')
|
125
|
+
if idx + 1 < len(argv):
|
126
|
+
return argv[idx + 1]
|
127
|
+
except (ValueError, IndexError):
|
128
|
+
pass
|
129
|
+
return "staging"
|
130
|
+
|
131
|
+
def _emit_telemetry(self) -> None:
|
132
|
+
"""Emit OpenTelemetry span for apex instance."""
|
133
|
+
if telemetry_manager is None or not hasattr(telemetry_manager, "is_enabled"):
|
134
|
+
logger.debug("Telemetry manager not available")
|
135
|
+
return
|
136
|
+
|
137
|
+
if not telemetry_manager.is_enabled():
|
138
|
+
logger.debug("Telemetry is not enabled")
|
139
|
+
return
|
140
|
+
|
141
|
+
# Calculate duration
|
142
|
+
duration_ms = 0
|
143
|
+
if self.start_time and self.end_time:
|
144
|
+
duration_ms = int((self.end_time - self.start_time) * 1000)
|
145
|
+
|
146
|
+
# Determine status
|
147
|
+
status = "completed" if self.exit_code == 0 else "failed"
|
148
|
+
success = self.exit_code == 0
|
149
|
+
|
150
|
+
# Build attributes for apex.instance span
|
151
|
+
attributes: Dict[str, Any] = {
|
152
|
+
"zen.instance.type": "apex",
|
153
|
+
"zen.instance.name": "apex.instance",
|
154
|
+
"zen.instance.status": status,
|
155
|
+
"zen.instance.success": success,
|
156
|
+
"zen.instance.duration_ms": duration_ms,
|
157
|
+
"zen.instance.exit_code": self.exit_code or 0,
|
158
|
+
"zen.apex.environment": self.env,
|
159
|
+
"zen.apex.message": self._truncate_message(self.message or ""),
|
160
|
+
}
|
161
|
+
|
162
|
+
# Parse JSON output if available (contains token/cost info)
|
163
|
+
json_output = self._parse_json_output()
|
164
|
+
if json_output:
|
165
|
+
self._add_json_metrics(attributes, json_output)
|
166
|
+
|
167
|
+
# Emit span using the telemetry manager's tracer (same way as regular zen instances)
|
168
|
+
try:
|
169
|
+
# Access the tracer the same way telemetry_manager.record_instance_span() does
|
170
|
+
if not hasattr(telemetry_manager, '_tracer') or telemetry_manager._tracer is None:
|
171
|
+
logger.warning("Telemetry manager has no tracer configured")
|
172
|
+
return
|
173
|
+
|
174
|
+
from opentelemetry.trace import SpanKind
|
175
|
+
from google.api_core.exceptions import GoogleAPICallError
|
176
|
+
|
177
|
+
with telemetry_manager._tracer.start_as_current_span(
|
178
|
+
"apex.instance", kind=SpanKind.INTERNAL
|
179
|
+
) as span:
|
180
|
+
for key, value in attributes.items():
|
181
|
+
span.set_attribute(key, value)
|
182
|
+
|
183
|
+
logger.info(f"✅ Emitted apex telemetry span with {len(attributes)} attributes")
|
184
|
+
logger.debug(f"Apex span attributes: {attributes}")
|
185
|
+
|
186
|
+
# Note: Removed force_flush to prevent blocking event streaming
|
187
|
+
# Spans will still be sent via the normal batch export process
|
188
|
+
|
189
|
+
except Exception as exc:
|
190
|
+
logger.error(f"❌ Failed to emit apex telemetry span: {exc}")
|
191
|
+
import traceback
|
192
|
+
logger.debug(f"Traceback: {traceback.format_exc()}")
|
193
|
+
|
194
|
+
def _truncate_message(self, message: str, max_length: int = 200) -> str:
|
195
|
+
"""Truncate message for telemetry attributes."""
|
196
|
+
if len(message) <= max_length:
|
197
|
+
return message
|
198
|
+
return message[:max_length] + "..."
|
199
|
+
|
200
|
+
def _parse_json_output(self) -> Optional[Dict[str, Any]]:
|
201
|
+
"""Parse JSON output from agent_cli stdout if available."""
|
202
|
+
if not self.stdout:
|
203
|
+
return None
|
204
|
+
|
205
|
+
# Try to find JSON in stdout
|
206
|
+
for line in self.stdout.split('\n'):
|
207
|
+
line = line.strip()
|
208
|
+
if line.startswith('{') and line.endswith('}'):
|
209
|
+
try:
|
210
|
+
return json.loads(line)
|
211
|
+
except json.JSONDecodeError:
|
212
|
+
continue
|
213
|
+
|
214
|
+
return None
|
215
|
+
|
216
|
+
def _add_json_metrics(self, attributes: Dict[str, Any], json_output: Dict[str, Any]) -> None:
|
217
|
+
"""Add metrics from JSON output to telemetry attributes."""
|
218
|
+
# Extract token usage if available
|
219
|
+
if 'usage' in json_output:
|
220
|
+
usage = json_output['usage']
|
221
|
+
attributes['zen.tokens.total'] = usage.get('total_tokens', 0)
|
222
|
+
attributes['zen.tokens.input'] = usage.get('input_tokens', 0)
|
223
|
+
attributes['zen.tokens.output'] = usage.get('output_tokens', 0)
|
224
|
+
attributes['zen.tokens.cache.read'] = usage.get('cache_read_tokens', 0)
|
225
|
+
attributes['zen.tokens.cache.creation'] = usage.get('cache_creation_tokens', 0)
|
226
|
+
|
227
|
+
# Extract cost if available
|
228
|
+
if 'cost' in json_output:
|
229
|
+
cost = json_output['cost']
|
230
|
+
if 'total_usd' in cost:
|
231
|
+
attributes['zen.cost.usd_total'] = round(float(cost['total_usd']), 6)
|
232
|
+
|
233
|
+
# Extract run_id if available
|
234
|
+
if 'run_id' in json_output:
|
235
|
+
attributes['zen.apex.run_id'] = json_output['run_id']
|
236
|
+
|
237
|
+
# Extract validation status
|
238
|
+
if 'validation' in json_output:
|
239
|
+
validation = json_output['validation']
|
240
|
+
attributes['zen.apex.validation.passed'] = validation.get('passed', False)
|
241
|
+
|
242
|
+
|
243
|
+
def run_apex_with_telemetry(
|
244
|
+
agent_cli_path: str,
|
245
|
+
filtered_argv: list,
|
246
|
+
env: Optional[Dict[str, str]] = None
|
247
|
+
) -> int:
|
248
|
+
"""Convenience function to run apex with telemetry tracking.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
agent_cli_path: Path to agent_cli.py script
|
252
|
+
filtered_argv: Command-line arguments (without 'zen' and '--apex')
|
253
|
+
env: Environment variables to pass to subprocess
|
254
|
+
|
255
|
+
Returns:
|
256
|
+
Exit code from agent_cli subprocess
|
257
|
+
"""
|
258
|
+
wrapper = ApexTelemetryWrapper()
|
259
|
+
return wrapper.run_apex_with_telemetry(agent_cli_path, filtered_argv, env)
|