whatap-python 2.1.4rc1__py3-none-any.whl → 2.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.
- whatap/__init__.py +376 -109
- whatap/agent/linux/amd64/whatap_python +0 -0
- whatap/agent/linux/arm64/whatap_python +0 -0
- whatap/build.py +2 -2
- whatap/conf/configuration.py +235 -4
- whatap/conf/configure.py +55 -0
- whatap/counter/__init__.py +26 -4
- whatap/counter/tasks/base_task.py +11 -2
- whatap/counter/tasks/llm_evaluator_task.py +56 -10
- whatap/counter/tasks/llm_log_sink_task.py +85 -19
- whatap/counter/tasks/llm_stat_task.py +73 -3
- whatap/doctor.py +402 -0
- whatap/llm/__init__.py +54 -1
- whatap/llm/api/__init__.py +20 -0
- whatap/llm/api/agent_name.py +83 -0
- whatap/llm/{prompt_meta.py → api/prompt_meta.py} +31 -4
- whatap/llm/api/session.py +175 -0
- whatap/llm/api/turn.py +88 -0
- whatap/llm/api/user.py +176 -0
- whatap/llm/api/workflow.py +115 -0
- whatap/llm/control/__init__.py +59 -0
- whatap/llm/control/cut.py +519 -0
- whatap/llm/control/guard.py +217 -0
- whatap/llm/control/trajectory.py +524 -0
- whatap/llm/definitions.py +21 -1
- whatap/llm/evaluators/__init__.py +4 -0
- whatap/llm/evaluators/base.py +15 -11
- whatap/llm/evaluators/builtins/_json_repair.py +106 -0
- whatap/llm/evaluators/builtins/combined_judge.py +7 -4
- whatap/llm/evaluators/builtins/judge_factories.py +90 -0
- whatap/llm/evaluators/builtins/llm_judge.py +107 -375
- whatap/llm/evaluators/builtins/pii_leak.py +5 -20
- whatap/llm/evaluators/builtins/url_scan.py +21 -22
- whatap/llm/evaluators/registry.py +91 -0
- whatap/llm/evaluators/scope.py +22 -2
- whatap/llm/instrument/__init__.py +32 -0
- whatap/llm/instrument/_boundary.py +606 -0
- whatap/llm/instrument/agent.py +35 -0
- whatap/llm/instrument/llm_api.py +89 -0
- whatap/llm/instrument/workflow.py +42 -0
- whatap/llm/log_sink_packs/llm_log_sink_pack.py +32 -0
- whatap/llm/log_sink_packs/llm_step_status.py +9 -0
- whatap/llm/log_sink_packs/llm_tx_status.py +51 -0
- whatap/llm/orchestration/__init__.py +16 -0
- whatap/llm/orchestration/hermes/__init__.py +143 -0
- whatap/llm/orchestration/hermes/gateway.py +172 -0
- whatap/llm/orchestration/hermes/kanban.py +353 -0
- whatap/llm/orchestration/langgraph/__init__.py +134 -0
- whatap/llm/pricing.py +89 -123
- whatap/llm/providers/anthropic/__init__.py +50 -15
- whatap/llm/providers/anthropic/messages/messages.py +3 -2
- whatap/llm/providers/anthropic/messages/messages_context.py +48 -0
- whatap/llm/providers/anthropic/messages/messages_extractor.py +53 -0
- whatap/llm/providers/anthropic/messages/messages_stream.py +163 -0
- whatap/llm/providers/bedrock/__init__.py +92 -0
- whatap/llm/providers/bedrock/_content.py +81 -0
- whatap/llm/providers/bedrock/converse/__init__.py +2 -0
- whatap/llm/providers/bedrock/converse/converse_context.py +155 -0
- whatap/llm/providers/bedrock/converse/converse_extractor.py +196 -0
- whatap/llm/providers/bedrock/event_stream.py +254 -0
- whatap/llm/providers/bedrock/http.py +395 -0
- whatap/llm/providers/bedrock/invoke_model/__init__.py +2 -0
- whatap/llm/providers/bedrock/invoke_model/invoke_model_context.py +126 -0
- whatap/llm/providers/bedrock/invoke_model/invoke_model_extractor.py +763 -0
- whatap/llm/providers/bedrock/runtime.py +291 -0
- whatap/llm/providers/bedrock/step.py +76 -0
- whatap/llm/providers/interceptor.py +182 -56
- whatap/llm/providers/openai/__init__.py +47 -70
- whatap/llm/providers/openai/chat/chat.py +71 -14
- whatap/llm/providers/openai/chat/chat_context.py +6 -0
- whatap/llm/providers/openai/chat/chat_extractor.py +8 -1
- whatap/llm/providers/openai/completions/completions.py +3 -2
- whatap/llm/providers/openai/content_parser.py +4 -1
- whatap/llm/providers/openai/embeddings/embeddings.py +3 -2
- whatap/llm/providers/openai/responses/responses.py +3 -2
- whatap/llm/providers/stream_accumulator.py +162 -4
- whatap/llm/providers/workflow.py +763 -0
- whatap/llm/stats/__init__.py +6 -0
- whatap/llm/stats/active_agent_stat.py +93 -0
- whatap/llm/stats/active_stat.py +48 -51
- whatap/llm/stats/actx_stat.py +80 -0
- whatap/llm/stats/base_stat.py +6 -0
- whatap/llm/stats/eval_stat.py +1 -0
- whatap/llm/stats/pii_leak_eval_stat.py +2 -1
- whatap/llm/stats/traj_actx_stat.py +133 -0
- whatap/llm/stats/url_scan_eval_stat.py +2 -1
- whatap/net/async_sender.py +242 -23
- whatap/net/packet_enum.py +1 -1
- whatap/net/param_def.py +17 -0
- whatap/net/udp_session.py +461 -34
- whatap/net/udp_thread.py +51 -2
- whatap/scripts/__init__.py +89 -0
- whatap/skills/whatap-python-install/AGENTS.md +29 -0
- whatap/skills/whatap-python-install/INSTALL.md +96 -0
- whatap/skills/whatap-python-install/SKILL.md +42 -0
- whatap/skills/whatap-python-install/references/config-keys.md +59 -0
- whatap/skills/whatap-python-install/references/frameworks.md +55 -0
- whatap/skills/whatap-python-install/references/llm-api-usage.md +81 -0
- whatap/skills/whatap-python-install/references/llm-evaluation.md +114 -0
- whatap/skills/whatap-python-install/references/llm-observability.md +123 -0
- whatap/skills/whatap-python-install/references/troubleshooting.md +41 -0
- whatap/trace/mod/application/fastapi/instrumentation.py +264 -65
- whatap/trace/mod/application/fastapi/scope.py +17 -0
- whatap/trace/mod/application/wsgi.py +19 -1
- whatap/trace/mod/hook_service_patterns.py +8 -54
- whatap/trace/mod/httpc/httpx.py +26 -3
- whatap/trace/mod/httpc/httpx2.py +23 -0
- whatap/trace/mod/httpc/requests.py +16 -3
- whatap/trace/mod/httpc/stream_end.py +136 -0
- whatap/trace/mod/httpc/util.py +594 -75
- whatap/trace/mod/pattern_targets.py +137 -0
- whatap/trace/mod/plugin.py +22 -59
- whatap/trace/mod/process/__init__.py +0 -0
- whatap/trace/mod/process/subprocess_spawn.py +390 -0
- whatap/trace/mod/standalone/multiple.py +2 -1
- whatap/trace/mod/standalone/single.py +2 -1
- whatap/trace/mod/thread_context.py +202 -0
- whatap/trace/mod/workflow_plugin.py +33 -0
- whatap/trace/mtrace_env.py +368 -0
- whatap/trace/patch.py +98 -0
- whatap/trace/trace_context.py +119 -1
- whatap/trace/trace_context_manager.py +773 -23
- whatap/trace/trace_error.py +16 -3
- whatap/trace/trace_import.py +59 -22
- whatap/trace/trace_module_definition.py +43 -0
- whatap/util/native_process.py +458 -0
- {whatap_python-2.1.4rc1.dist-info → whatap_python-2.2.1.dist-info}/METADATA +1 -1
- {whatap_python-2.1.4rc1.dist-info → whatap_python-2.2.1.dist-info}/RECORD +131 -73
- {whatap_python-2.1.4rc1.dist-info → whatap_python-2.2.1.dist-info}/entry_points.txt +2 -0
- whatap/llm/stats/meter.py +0 -18
- {whatap_python-2.1.4rc1.dist-info → whatap_python-2.2.1.dist-info}/WHEEL +0 -0
- {whatap_python-2.1.4rc1.dist-info → whatap_python-2.2.1.dist-info}/top_level.txt +0 -0
whatap/__init__.py
CHANGED
|
@@ -117,6 +117,37 @@ def whatap_print(*args):
|
|
|
117
117
|
message = " ".join(args)
|
|
118
118
|
print(message)
|
|
119
119
|
|
|
120
|
+
|
|
121
|
+
def whatap_error_log(msg, log_id='WHATAP_AGENT_ERROR'):
|
|
122
|
+
"""에이전트 시작 오류를 프로세스 로그(stdout)와 hook.log 양쪽에 동일하게 노출한다.
|
|
123
|
+
|
|
124
|
+
hook.log FileHandler 는 Configure.load(display=True)->Logger() 시점에 붙는데,
|
|
125
|
+
go() 스레드가 config() 보다 먼저 도는 경로(자동 attach)나 start_agent 경로에서는
|
|
126
|
+
아직 핸들러가 없을 수 있다. 그 경우에도 오류가 유실되지 않도록, 핸들러가 없으면
|
|
127
|
+
whatap-hook.log 에 직접 append 한다."""
|
|
128
|
+
# 1) 프로세스 로그 (사용자 애플리케이션 stdout)
|
|
129
|
+
whatap_print(msg)
|
|
130
|
+
|
|
131
|
+
# 2) hook.log
|
|
132
|
+
try:
|
|
133
|
+
if logging.handlers:
|
|
134
|
+
logging.error(msg, extra={'id': log_id})
|
|
135
|
+
return
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
# FileHandler 미부착: whatap-hook.log 에 직접 기록
|
|
140
|
+
try:
|
|
141
|
+
logs_dir = os.environ.get('WHATAP_LOGS') or \
|
|
142
|
+
os.path.join(os.environ.get('WHATAP_HOME', '.'), 'logs')
|
|
143
|
+
if not os.path.exists(logs_dir):
|
|
144
|
+
os.makedirs(logs_dir)
|
|
145
|
+
with open(os.path.join(logs_dir, LOG_FILE_NAME), 'a') as f:
|
|
146
|
+
f.write('[{0}] : {1} - {2}\n'.format(
|
|
147
|
+
time.strftime(LOGGING_DATE_FORMAT), log_id, msg))
|
|
148
|
+
except Exception as e:
|
|
149
|
+
whatap_print('WHATAP: hook.log write failed: {}'.format(e))
|
|
150
|
+
|
|
120
151
|
class Logger(object):
|
|
121
152
|
def __init__(self):
|
|
122
153
|
self.logger = logging
|
|
@@ -291,6 +322,18 @@ def config(home):
|
|
|
291
322
|
from whatap.conf.license import License
|
|
292
323
|
conf.PCODE = License.getProjectCode(conf.license)
|
|
293
324
|
|
|
325
|
+
# LLM 채널은 자기 프로젝트 코드를 가진다. LLM 트랜잭션(is_llm=1)은 LLM Go Agent
|
|
326
|
+
# 소켓으로만 나가므로 결합 좌표에 APM PCODE 를 실으면 서버가 호출자를 엉뚱한
|
|
327
|
+
# 프로젝트에서 찾는다(conf.pcode_of 참조). getProjectCode 는 키 문자열 파싱이라
|
|
328
|
+
# 서버 왕복이 없고, apm_enabled=false 인 LLM 전용 배포에서도 해소된다.
|
|
329
|
+
llm_key = str(getattr(conf, 'llm_accesskey', '') or '').strip()
|
|
330
|
+
if llm_key:
|
|
331
|
+
try:
|
|
332
|
+
from whatap.conf.license import License
|
|
333
|
+
conf.LLM_PCODE = License.getProjectCode(llm_key)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
whatap_print('WHATAP: LLM PCODE RESOLVE ERROR: {}'.format(e))
|
|
336
|
+
|
|
294
337
|
import whatap.counter
|
|
295
338
|
|
|
296
339
|
hooks(home)
|
|
@@ -335,11 +378,20 @@ def _is_instrumentation_ignored(definition_key, ignore_set):
|
|
|
335
378
|
def hooks(home):
|
|
336
379
|
ignore_set = _build_ignore_instrumentation_set()
|
|
337
380
|
skipped_keys = []
|
|
381
|
+
# LLM 계측은 llm_enabled 일 때만 **등록**한다. instrument_* 함수들도 각자
|
|
382
|
+
# llm_enabled 를 보고 되돌아가지만, 등록만 해둬도 대상 모듈이 로드되는 순간
|
|
383
|
+
# import 훅이 ``whatap.llm.*`` 서브트리를 통째로 끌어온다 — 켜지도 않은 기능
|
|
384
|
+
# 때문에. ``botocore.client``(Bedrock) 처럼 LLM 과 무관한 앱도 흔히 쓰는 모듈이
|
|
385
|
+
# 대상에 들어오면서 이 비용이 실제로 문제가 된다.
|
|
386
|
+
# conf.init() 이 hooks() 보다 먼저 돌므로 여기서 llm_enabled 는 이미 확정이다.
|
|
387
|
+
llm_enabled = bool(getattr(conf, 'llm_enabled', False))
|
|
338
388
|
try:
|
|
339
389
|
for key, value_list in DEFINITION.items():
|
|
340
390
|
if _is_instrumentation_ignored(key, ignore_set):
|
|
341
391
|
skipped_keys.append(key)
|
|
342
392
|
continue
|
|
393
|
+
if key.startswith('llm.') and not llm_enabled:
|
|
394
|
+
continue
|
|
343
395
|
for value in value_list:
|
|
344
396
|
if len(value) >= 3 and isinstance(value[2], str):
|
|
345
397
|
module_path = value[2]
|
|
@@ -366,47 +418,61 @@ def hooks(home):
|
|
|
366
418
|
from whatap.trace.mod.logging import instrument_logging
|
|
367
419
|
instrument_logging(logging_module)
|
|
368
420
|
|
|
421
|
+
# 한 모듈에 여러 패턴 계열(스텝=instrument_plugin / 워크플로우=instrument_workflow
|
|
422
|
+
# / 트랜잭션=instrument_service)이 공존할 수 있도록, IMPORT_HOOKS[모듈]에
|
|
423
|
+
# 인스트루먼터를 각자의 class_defs 와 함께 리스트로 누적한다. 과거엔 단일 dict 라
|
|
424
|
+
# 뒤 계열이 앞을 덮어써서 같은 모듈에 두 패턴(예: predict=트랜잭션 + 헬퍼=스텝)을
|
|
425
|
+
# 못 걸었다.
|
|
426
|
+
def _register_pattern_hook(module_name, class_def, def_name, mod_path):
|
|
427
|
+
if not PLUGIN.get(module_name):
|
|
428
|
+
PLUGIN[module_name] = []
|
|
429
|
+
if class_def not in PLUGIN[module_name]:
|
|
430
|
+
PLUGIN[module_name].append(class_def) # 호환용(외부 참조)
|
|
431
|
+
existing = IMPORT_HOOKS.get(module_name)
|
|
432
|
+
if isinstance(existing, list):
|
|
433
|
+
entry_list = existing
|
|
434
|
+
elif isinstance(existing, dict):
|
|
435
|
+
entry_list = [existing]
|
|
436
|
+
IMPORT_HOOKS[module_name] = entry_list
|
|
437
|
+
else:
|
|
438
|
+
entry_list = []
|
|
439
|
+
IMPORT_HOOKS[module_name] = entry_list
|
|
440
|
+
for e in entry_list:
|
|
441
|
+
if isinstance(e, dict) and e.get('def') == def_name \
|
|
442
|
+
and e.get('module') == mod_path:
|
|
443
|
+
e.setdefault('class_defs', []).append(class_def)
|
|
444
|
+
return
|
|
445
|
+
entry_list.append({'def': def_name, 'module': mod_path,
|
|
446
|
+
'class_defs': [class_def]})
|
|
447
|
+
|
|
369
448
|
if conf.hook_method_patterns:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
module_name, class_def
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
'mod',
|
|
390
|
-
key)}
|
|
391
|
-
|
|
392
|
-
# 서비스(트랜잭션) 진입점 자동 래핑 (conf 방법). hook_method_patterns 와 같은
|
|
393
|
-
# import-hook 플러밍, 래퍼만 트랜잭션 open/close (whatap.trace.mod.hook_service_patterns).
|
|
394
|
-
# @method_profiling 을 소스 수정 없이 conf 로 적용하는 경로.
|
|
449
|
+
for pattern in conf.hook_method_patterns.split(','):
|
|
450
|
+
pattern = pattern.strip()
|
|
451
|
+
if not pattern or ':' not in pattern:
|
|
452
|
+
continue
|
|
453
|
+
module_name, class_def = pattern.split(':', 1)
|
|
454
|
+
_register_pattern_hook(module_name.strip(), class_def.strip(),
|
|
455
|
+
'instrument_plugin', 'whatap.trace.mod.plugin')
|
|
456
|
+
|
|
457
|
+
# LLM 워크플로우 경계 자동 래핑 (conf 방법). 래퍼만 워크플로우 open/close.
|
|
458
|
+
if conf.workflow_method_patterns:
|
|
459
|
+
for pattern in conf.workflow_method_patterns.split(','):
|
|
460
|
+
pattern = pattern.strip()
|
|
461
|
+
if not pattern or ':' not in pattern:
|
|
462
|
+
continue
|
|
463
|
+
module_name, class_def = pattern.split(':', 1)
|
|
464
|
+
_register_pattern_hook(module_name.strip(), class_def.strip(),
|
|
465
|
+
'instrument_workflow', 'whatap.trace.mod.workflow_plugin')
|
|
466
|
+
|
|
467
|
+
# 서비스(트랜잭션) 진입점 자동 래핑 (conf 방법). @method_profiling 의 conf 버전.
|
|
395
468
|
if conf.hook_service_patterns:
|
|
396
469
|
for pattern in conf.hook_service_patterns.split(','):
|
|
397
470
|
pattern = pattern.strip()
|
|
398
471
|
if not pattern or ':' not in pattern:
|
|
399
472
|
continue
|
|
400
473
|
module_name, class_def = pattern.split(':', 1)
|
|
401
|
-
module_name
|
|
402
|
-
|
|
403
|
-
if not PLUGIN.get(module_name):
|
|
404
|
-
PLUGIN[module_name] = []
|
|
405
|
-
PLUGIN[module_name].append(class_def)
|
|
406
|
-
# 대상 모듈이 PLUGIN 에 등록되면 ImportFinder 가 class_defs 패키징.
|
|
407
|
-
IMPORT_HOOKS[module_name] = {
|
|
408
|
-
'def': 'instrument_service',
|
|
409
|
-
'module': 'whatap.trace.mod.hook_service_patterns'}
|
|
474
|
+
_register_pattern_hook(module_name.strip(), class_def.strip(),
|
|
475
|
+
'instrument_service', 'whatap.trace.mod.hook_service_patterns')
|
|
410
476
|
|
|
411
477
|
if conf.standalone_enabled:
|
|
412
478
|
if conf.standalone_type == 'multiple-transaction':
|
|
@@ -420,9 +486,43 @@ def hooks(home):
|
|
|
420
486
|
logging.debug(e, extra={'id': 'PLUGIN ERROR'})
|
|
421
487
|
finally:
|
|
422
488
|
sys.meta_path.insert(0, ImportFinder())
|
|
489
|
+
_instrument_already_imported()
|
|
423
490
|
logging.debug('WHATAP AGENT START!', extra={'id': 'WA000'})
|
|
424
491
|
|
|
425
492
|
|
|
493
|
+
def _instrument_already_imported():
|
|
494
|
+
"""finder 설치 시점에 **이미 import 돼 있던** 훅 대상을 직접 계측한다.
|
|
495
|
+
|
|
496
|
+
``ImportFinder.find_spec`` 은 import 시스템이 ``sys.modules`` 미스일 때만 호출하므로,
|
|
497
|
+
finder 설치보다 먼저 로드된 모듈은 훅이 영원히 안 걸린다. 그리고 이 에이전트는 자기
|
|
498
|
+
부팅에 필요한 모듈(``subprocess`` — 최상단 import, 데몬 기동에 사용)을 finder 설치
|
|
499
|
+
**전에** 로드하므로, 그 대상은 DEFINITION 에 등록해도 계측되지 않는다.
|
|
500
|
+
|
|
501
|
+
앱이 우회할 수 없는 종류의 구멍이다(앱이 할 수 있는 건 에이전트 내부 instrument_*
|
|
502
|
+
함수를 대신 호출하는 것뿐이고, 그건 자동계측을 쓰는 의미를 없앤다).
|
|
503
|
+
|
|
504
|
+
``load_module`` 을 그대로 재사용하므로 정상 import 경로와 완전히 같은 처리를 받는다
|
|
505
|
+
(엔트리 리스트/class_defs 패키징 포함). instrument_* 는 멱등이어야 한다 — 이 스윕과 이후의
|
|
506
|
+
재-import 가 같은 대상을 두 번 건드릴 수 있다.
|
|
507
|
+
"""
|
|
508
|
+
try:
|
|
509
|
+
from whatap.trace.trace_import import load_module
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logging.info("pre-imported sweep unavailable: %s: %s",
|
|
512
|
+
e.__class__.__name__, e, exc_info=True)
|
|
513
|
+
return
|
|
514
|
+
for fullname in list(IMPORT_HOOKS.keys()):
|
|
515
|
+
module = sys.modules.get(fullname)
|
|
516
|
+
if module is None:
|
|
517
|
+
continue
|
|
518
|
+
try:
|
|
519
|
+
load_module(module, fullname)
|
|
520
|
+
except Exception as e:
|
|
521
|
+
# 계측 실패는 조용히 넘기면 "데이터가 안 보인다" 로만 드러난다.
|
|
522
|
+
logging.info("failed to inject pre-imported %s: %s: %s",
|
|
523
|
+
fullname, e.__class__.__name__, e, exc_info=True)
|
|
524
|
+
|
|
525
|
+
|
|
426
526
|
def agent():
|
|
427
527
|
home = 'WHATAP_HOME'
|
|
428
528
|
whatap_home = os.environ.get(home)
|
|
@@ -446,14 +546,28 @@ def agent():
|
|
|
446
546
|
llm_enabled = preview_whatap_conf("llm_enabled")
|
|
447
547
|
|
|
448
548
|
if apm_enabled == 'true':
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
549
|
+
if sys.platform.startswith('linux'):
|
|
550
|
+
# Publish native ownership before a short-lived interpreter
|
|
551
|
+
# can exit. This does not wait for collector/TCP readiness.
|
|
552
|
+
go()
|
|
553
|
+
else:
|
|
554
|
+
t = threading.Thread(target=go)
|
|
555
|
+
t.setDaemon(True)
|
|
556
|
+
t.start()
|
|
452
557
|
|
|
453
558
|
if llm_enabled == 'true':
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
559
|
+
# llm_enabled=true 라도 LLM 채널 자격증명(llm_accesskey)이 없으면 LLM
|
|
560
|
+
# 에이전트를 띄우지 않는다. (env 로 플래그만 켜진 경우의 오작동 방지 —
|
|
561
|
+
# llm_accesskey/LLM 서버 호스트 없이 LLM 모드로 도는 것을 막는다.)
|
|
562
|
+
llm_accesskey = (preview_whatap_conf("llm_accesskey")
|
|
563
|
+
or os.environ.get("llm_accesskey"))
|
|
564
|
+
if llm_accesskey:
|
|
565
|
+
t = threading.Thread(target=go, kwargs={'llm': True})
|
|
566
|
+
t.setDaemon(True)
|
|
567
|
+
t.start()
|
|
568
|
+
else:
|
|
569
|
+
whatap_print("WHATAP: llm_enabled=true 이나 llm_accesskey 가 "
|
|
570
|
+
"없어 LLM 에이전트 기동을 건너뜁니다")
|
|
457
571
|
|
|
458
572
|
config(home)
|
|
459
573
|
|
|
@@ -464,14 +578,135 @@ ARCH = {
|
|
|
464
578
|
'ARM': 'arm',
|
|
465
579
|
'AArch64': 'arm64',
|
|
466
580
|
'arm64': 'arm64',
|
|
467
|
-
'aarch64': 'arm64'
|
|
581
|
+
'aarch64': 'arm64',
|
|
582
|
+
# Windows 의 platform.machine() 은 **대문자**로 돌려준다('AMD64'/'ARM64').
|
|
583
|
+
# Windows 는 arch 하위 디렉토리를 쓰지 않아 지금은 이 표를 안 타지만, 빠져 있으면
|
|
584
|
+
# 누가 조회하는 순간 KeyError 로 기동이 통째로 죽는 종류의 구멍이라 채워 둔다.
|
|
585
|
+
'AMD64': 'amd64',
|
|
586
|
+
'ARM64': 'arm64',
|
|
587
|
+
'i386': '386',
|
|
468
588
|
}
|
|
469
589
|
|
|
470
590
|
AGENT_NAME = 'whatap_python'
|
|
471
591
|
|
|
472
592
|
|
|
593
|
+
def agent_binary_name():
|
|
594
|
+
"""OS 별 에이전트 실행 파일 이름."""
|
|
595
|
+
return AGENT_NAME + '.exe' if sys.platform == 'win32' else AGENT_NAME
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def llm_agent_reldir():
|
|
599
|
+
"""LLM 전용 ``whatap_python`` 이 놓이는 ``$WHATAP_HOME`` 하위 상대경로.
|
|
600
|
+
|
|
601
|
+
Unix 계열은 ``<os>/<arch>`` (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64).
|
|
602
|
+
**Windows 는 arch 하위를 쓰지 않는다** — x64 하나면 ARM64 Windows 에서도 에뮬레이션
|
|
603
|
+
으로 돌아서 아키텍처별 빌드를 나눌 이유가 없다. 번들 APM 쪽
|
|
604
|
+
``whatap/agent/windows/`` 가 평평한 것과 같은 이유이고, 같은 모양이다.
|
|
605
|
+
|
|
606
|
+
반환: ``(상대경로, 사유)``. 상대경로가 None 이면 사유에 왜 못 구하는지가 담긴다.
|
|
607
|
+
"""
|
|
608
|
+
system = platform.system().lower()
|
|
609
|
+
if sys.platform == 'win32':
|
|
610
|
+
return system, ''
|
|
611
|
+
machine = platform.machine()
|
|
612
|
+
arch = ARCH.get(machine)
|
|
613
|
+
if not arch:
|
|
614
|
+
return None, ('unsupported architecture {0!r} for the LLM agent '
|
|
615
|
+
'(known: {1})'.format(machine, ', '.join(sorted(set(ARCH.values())))))
|
|
616
|
+
return os.path.join(system, arch), ''
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def llm_agent_path():
|
|
620
|
+
"""LLM 전용 ``whatap_python`` 의 배포 위치. @see ``llm_agent_reldir``
|
|
621
|
+
|
|
622
|
+
반환: ``(경로, 사유)``. 경로가 None 이면 사유에 왜 못 쓰는지가 담긴다.
|
|
623
|
+
"""
|
|
624
|
+
reldir, reason = llm_agent_reldir()
|
|
625
|
+
if reldir is None:
|
|
626
|
+
return None, reason
|
|
627
|
+
home_path = os.environ.get('WHATAP_HOME')
|
|
628
|
+
if not home_path:
|
|
629
|
+
return None, 'WHATAP_HOME is not set'
|
|
630
|
+
path = os.path.join(home_path, reldir, agent_binary_name())
|
|
631
|
+
if not os.path.exists(path):
|
|
632
|
+
return None, ('LLM agent binary not found: {0} '
|
|
633
|
+
'(place the LLM {1} under $WHATAP_HOME/{2}/)'.format(
|
|
634
|
+
path, agent_binary_name(), reldir.replace(os.sep, '/')))
|
|
635
|
+
return path, ''
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _native_agent_spec(opts):
|
|
640
|
+
from whatap.util.native_process import AgentSpec
|
|
641
|
+
source = os.path.join(os.path.dirname(__file__), 'agent', 'linux',
|
|
642
|
+
ARCH[platform.machine()], AGENT_NAME)
|
|
643
|
+
env = os.environ.copy()
|
|
644
|
+
env.update(opts)
|
|
645
|
+
return AgentSpec(os.environ['WHATAP_HOME'], source, env)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _native_agent_log_path():
|
|
649
|
+
return os.path.join(os.environ.get('WHATAP_HOME', '.'), 'logs', AGENT_NAME + '.log')
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _open_native_agent_log():
|
|
653
|
+
# managed 경로는 PIPE 를 읽지 않으므로 네이티브 stdout/stderr 를 파일로 남겨
|
|
654
|
+
# 기동 직후 실패 원인을 추적할 수 있게 한다.
|
|
655
|
+
try:
|
|
656
|
+
log_path = _native_agent_log_path()
|
|
657
|
+
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
658
|
+
return open(log_path, 'ab')
|
|
659
|
+
except Exception as e:
|
|
660
|
+
whatap_print('WHATAP: native agent log open failed: %s' % e)
|
|
661
|
+
return subprocess.DEVNULL
|
|
662
|
+
|
|
473
663
|
|
|
474
664
|
def go(batch=False, opts={}, llm=False):
|
|
665
|
+
if sys.platform.startswith('linux') and not batch and not llm:
|
|
666
|
+
from whatap.util.native_process import FAILED, ensure_agent_running
|
|
667
|
+
|
|
668
|
+
def on_child_exit(pid, exit_code):
|
|
669
|
+
whatap_error_log(
|
|
670
|
+
'WHATAP: native APM exited (PID: {}, exit code: {}, log: {})'.format(
|
|
671
|
+
pid, exit_code, _native_agent_log_path()),
|
|
672
|
+
log_id='WHATAP_AGENT_EXITED')
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
spec = _native_agent_spec(opts)
|
|
676
|
+
result = ensure_agent_running(
|
|
677
|
+
spec, lambda: _start_go(batch, opts, llm, managed=True),
|
|
678
|
+
on_child_exit=on_child_exit)
|
|
679
|
+
if result.status == FAILED:
|
|
680
|
+
whatap_error_log('WHATAP: native APM startup failed',
|
|
681
|
+
log_id='WHATAP_AGENT_START_FAILED')
|
|
682
|
+
else:
|
|
683
|
+
whatap_print('WHATAP: native APM {} (PID: {})'.format(
|
|
684
|
+
result.status, result.pid))
|
|
685
|
+
except Exception as e:
|
|
686
|
+
whatap_error_log('WHATAP: native APM startup failed: {}'.format(e),
|
|
687
|
+
log_id='WHATAP_AGENT_START_FAILED')
|
|
688
|
+
return
|
|
689
|
+
return _start_go(batch, opts, llm)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _start_go(batch=False, opts={}, llm=False, managed=False):
|
|
693
|
+
# LLM 에이전트는 전용 whatap_python 이 $WHATAP_HOME 하위에 따로 배포된다
|
|
694
|
+
# (unix 는 <os>/<arch>, windows 는 windows/ — @see llm_agent_reldir).
|
|
695
|
+
# **OS 로 막지 않는다** — 그 자리에 바이너리가 놓였는지로 판단한다(그래야 windows
|
|
696
|
+
# 빌드가 배포되는 순간 코드 변경 없이 살아난다).
|
|
697
|
+
#
|
|
698
|
+
# 여기서 미리 걸러야 하는 이유: 그냥 흘려보내면 배포 분기로 빠져 **번들 APM
|
|
699
|
+
# 바이너리**를 골라 ``--llm`` 으로 띄우고, 심지어 APM 과 같은
|
|
700
|
+
# ``$WHATAP_HOME/whatap_python`` 에 배포해 서로를 덮는다(둘은 다른 바이너리다).
|
|
701
|
+
llm_agent_cmd = None
|
|
702
|
+
if llm:
|
|
703
|
+
llm_agent_cmd, reason = llm_agent_path()
|
|
704
|
+
if llm_agent_cmd is None:
|
|
705
|
+
whatap_error_log(
|
|
706
|
+
'WHATAP: skipping LLM agent start — {0}'.format(reason),
|
|
707
|
+
log_id='WHATAP_LLM_AGENT_UNAVAILABLE')
|
|
708
|
+
return
|
|
709
|
+
|
|
475
710
|
newenv=os.environ.copy()
|
|
476
711
|
newenv['WHATAP_VERSION'] = build.version
|
|
477
712
|
newenv['whatap.start'] = str(DateUtil.now())
|
|
@@ -514,7 +749,7 @@ def go(batch=False, opts={}, llm=False):
|
|
|
514
749
|
return ''
|
|
515
750
|
|
|
516
751
|
pid = read_file(home, file_name)
|
|
517
|
-
if pid and get_pname(pid).find('whatap_python') >= 0:
|
|
752
|
+
if not managed and pid and get_pname(pid).find('whatap_python') >= 0:
|
|
518
753
|
if sys.platform == 'win32':
|
|
519
754
|
# Windows: Don't kill, just skip
|
|
520
755
|
whatap_print('WHATAP: Agent already running (PID: {}). Skipping duplicate execution.'.format(pid))
|
|
@@ -531,48 +766,75 @@ def go(batch=False, opts={}, llm=False):
|
|
|
531
766
|
home_path= os.environ.get(home)
|
|
532
767
|
|
|
533
768
|
# Windows uses .exe extension
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
#
|
|
538
|
-
#
|
|
539
|
-
#
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
769
|
+
binary_name = agent_binary_name()
|
|
770
|
+
|
|
771
|
+
# LLM mode runs a dedicated whatap_python binary that ships separately under
|
|
772
|
+
# $WHATAP_HOME (unix: <os>/<arch>, windows: windows/ — @see llm_agent_reldir).
|
|
773
|
+
# It is executed in place from there rather than deployed
|
|
774
|
+
# from the bundled package into $WHATAP_HOME/whatap_python, which the APM
|
|
775
|
+
# agent uses (the two are distinct binaries and would otherwise clobber each
|
|
776
|
+
# other). **OS 로 가르지 않는다** — Windows 만 빼면 거기서 APM 바이너리를
|
|
777
|
+
# 골라 --llm 으로 띄우고 APM 의 배포 경로까지 덮어쓴다.
|
|
778
|
+
run_in_place = llm
|
|
779
|
+
|
|
780
|
+
if run_in_place:
|
|
781
|
+
# 경로·존재 확인은 _start_go() 진입부의 llm_agent_path() 가 이미 했다(단일 소스).
|
|
782
|
+
source_agent = llm_agent_cmd
|
|
783
|
+
# Ensure executable (best effort; ignore on read-only locations)
|
|
784
|
+
if not os.access(source_agent, os.X_OK):
|
|
785
|
+
try:
|
|
786
|
+
os.chmod(source_agent, 0o755)
|
|
787
|
+
except OSError as e:
|
|
788
|
+
whatap_print('WHATAP: chmod LLM agent binary failed: %s' % e)
|
|
789
|
+
agent_cmd = source_agent
|
|
552
790
|
else:
|
|
553
|
-
|
|
791
|
+
dest_agent = os.path.join(home_path, binary_name)
|
|
554
792
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
793
|
+
# Try to remove existing binary
|
|
794
|
+
# Windows: File may still be locked even after waiting, ignore error and use existing binary
|
|
795
|
+
# Unix/Linux: Removal should always succeed
|
|
796
|
+
needs_deployment = False
|
|
797
|
+
if os.path.exists(dest_agent):
|
|
798
|
+
try:
|
|
799
|
+
os.remove(dest_agent)
|
|
800
|
+
needs_deployment = True
|
|
801
|
+
except (OSError, PermissionError) as e:
|
|
802
|
+
if sys.platform == 'win32':
|
|
803
|
+
# Windows: File is still locked, use existing binary
|
|
804
|
+
pass
|
|
805
|
+
else:
|
|
806
|
+
# Unix/Linux: This should not happen
|
|
807
|
+
raise
|
|
808
|
+
else:
|
|
809
|
+
needs_deployment = True
|
|
561
810
|
|
|
562
|
-
|
|
811
|
+
# Windows doesn't use architecture subdirectories (only single x64 binary for now)
|
|
812
|
+
if sys.platform == 'win32':
|
|
813
|
+
source_cwd = os.path.join(os.path.dirname(__file__), 'agent', 'windows')
|
|
814
|
+
else:
|
|
815
|
+
source_cwd = os.path.join(os.path.join(os.path.dirname(__file__), 'agent'), platform.system().lower(),
|
|
816
|
+
ARCH[platform.machine()])
|
|
563
817
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
#
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
818
|
+
source_agent = os.path.join(source_cwd, binary_name)
|
|
819
|
+
|
|
820
|
+
# Deploy agent binary only if needed
|
|
821
|
+
if needs_deployment:
|
|
822
|
+
# Try symlink first (Unix/Linux or Windows with proper permissions)
|
|
823
|
+
try:
|
|
824
|
+
os.symlink(source_agent, dest_agent)
|
|
825
|
+
except (OSError, NotImplementedError):
|
|
826
|
+
# Fallback to copy on Windows or when symlink fails
|
|
827
|
+
import shutil
|
|
828
|
+
shutil.copy2(source_agent, dest_agent)
|
|
829
|
+
# Make executable on Unix-like systems
|
|
830
|
+
if sys.platform != 'win32':
|
|
831
|
+
os.chmod(dest_agent, 0o755)
|
|
832
|
+
|
|
833
|
+
# Windows uses absolute path, Unix/Linux uses relative path
|
|
834
|
+
if sys.platform == 'win32':
|
|
835
|
+
agent_cmd = os.path.join(home_path, binary_name)
|
|
836
|
+
else:
|
|
837
|
+
agent_cmd = './{0}'.format(binary_name)
|
|
576
838
|
|
|
577
839
|
sockfile_path = os.path.join(home_path, 'run')
|
|
578
840
|
if not os.path.exists(sockfile_path):
|
|
@@ -581,12 +843,6 @@ def go(batch=False, opts={}, llm=False):
|
|
|
581
843
|
newenv['WHATAP_PID_FILE'] = file_name
|
|
582
844
|
newenv['PYTHON_PARENT_APP_PID'] = str(os.getpid())
|
|
583
845
|
|
|
584
|
-
# Windows uses absolute path, Unix/Linux uses relative path
|
|
585
|
-
if sys.platform == 'win32':
|
|
586
|
-
agent_cmd = os.path.join(home_path, agent_binary_name)
|
|
587
|
-
else:
|
|
588
|
-
agent_cmd = './{0}'.format(agent_binary_name)
|
|
589
|
-
|
|
590
846
|
# Windows: Run in background with DETACHED_PROCESS
|
|
591
847
|
# Unix/Linux: Run in background with start_new_session
|
|
592
848
|
# 실행 인자 구성
|
|
@@ -638,6 +894,17 @@ def go(batch=False, opts={}, llm=False):
|
|
|
638
894
|
cwd=home_path, env=newenv,
|
|
639
895
|
creationflags=DETACHED_PROCESS,
|
|
640
896
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
897
|
+
elif managed:
|
|
898
|
+
go_out = _open_native_agent_log()
|
|
899
|
+
try:
|
|
900
|
+
process = subprocess.Popen(cmd_args,
|
|
901
|
+
cwd=home_path, env=newenv,
|
|
902
|
+
start_new_session=True,
|
|
903
|
+
stdout=go_out, stderr=go_out)
|
|
904
|
+
finally:
|
|
905
|
+
if go_out is not subprocess.DEVNULL:
|
|
906
|
+
go_out.close()
|
|
907
|
+
return process
|
|
641
908
|
else:
|
|
642
909
|
process = subprocess.Popen(cmd_args,
|
|
643
910
|
cwd=home_path, env=newenv,
|
|
@@ -657,7 +924,10 @@ def go(batch=False, opts={}, llm=False):
|
|
|
657
924
|
except Exception as e:
|
|
658
925
|
import traceback
|
|
659
926
|
traceback.print_exc()
|
|
660
|
-
|
|
927
|
+
# hook.log + 프로세스 로그 양쪽에 동일하게 노출
|
|
928
|
+
whatap_error_log(
|
|
929
|
+
'WHATAP: {0} AGENT ERROR: {1}'.format('LLM' if llm else 'APM', e),
|
|
930
|
+
log_id='WHATAP_LLM_AGENT_ERROR' if llm else 'WHATAP_AGENT_ERROR')
|
|
661
931
|
else:
|
|
662
932
|
whatap_print('WHATAP: AGENT UP! (process name: {}{})\n'.format(AGENT_NAME, ' --llm' if llm else ''))
|
|
663
933
|
|
|
@@ -695,8 +965,8 @@ def _service_trace(fn, service_name=None):
|
|
|
695
965
|
"""fn 을 하나의 트랜잭션(TX_START~TX_END)으로 감싼 래퍼를 반환한다.
|
|
696
966
|
|
|
697
967
|
``@method_profiling`` 데코레이터와 conf ``hook_service_patterns``
|
|
698
|
-
(whatap.trace.mod.hook_service_patterns) 의 **공용 코어**. 여기엔 agent() 부팅이
|
|
699
|
-
|
|
968
|
+
(whatap.trace.mod.hook_service_patterns) 의 **공용 코어**. 여기엔 agent() 부팅이 없다
|
|
969
|
+
(데코레이터/부트 경로가 각자 처리).
|
|
700
970
|
|
|
701
971
|
코루틴 함수는 동기 래퍼로 감싸면 트랜잭션이 start_interceptor → 코루틴 생성 →
|
|
702
972
|
end_interceptor 로 즉시 열렸다 닫히고, 실제 본문은 호출자가 await 할 때 트랜잭션
|
|
@@ -772,26 +1042,7 @@ def _service_trace(fn, service_name=None):
|
|
|
772
1042
|
|
|
773
1043
|
|
|
774
1044
|
def method_profiling(fn):
|
|
775
|
-
|
|
776
|
-
# 공개 API 라, 예외 재전파/컨텍스트 정리/async 지원으로 바꾸면 기존 사용자의
|
|
777
|
-
# 동작이 달라진다. 해당 수정은 dev 에 있고 정식 머지 때 함께 들어온다.
|
|
778
|
-
def trace(*args, **kwargs):
|
|
779
|
-
callback = None
|
|
780
|
-
try:
|
|
781
|
-
ctx = TraceContext()
|
|
782
|
-
ctx.service_name=fn.__name__
|
|
783
|
-
start_interceptor(ctx)
|
|
784
|
-
callback = fn(*args, **kwargs)
|
|
785
|
-
except Exception as e:
|
|
786
|
-
ctx = TraceContextManager.getLocalContext()
|
|
787
|
-
interceptor_step_error(e, ctx=ctx)
|
|
788
|
-
interceptor_error_log(ctx.id, e, fn, args, kwargs)
|
|
789
|
-
logging.debug('WHATAP(@method_profiling): ' + str(e),
|
|
790
|
-
extra={'id': 'WA776'}, exc_info=True)
|
|
791
|
-
finally:
|
|
792
|
-
ctx = TraceContextManager.getLocalContext()
|
|
793
|
-
end_interceptor(ctx=ctx)
|
|
794
|
-
return callback
|
|
1045
|
+
trace = _service_trace(fn)
|
|
795
1046
|
|
|
796
1047
|
if not os.environ.get('whatap.enabled'):
|
|
797
1048
|
agent()
|
|
@@ -966,6 +1217,22 @@ def get_port_number(port=6600, home=os.environ.get('WHATAP_HOME'), lock_file=Non
|
|
|
966
1217
|
|
|
967
1218
|
|
|
968
1219
|
def configPort():
|
|
1220
|
+
if sys.platform.startswith('linux') and preview_whatap_conf('apm_enabled') == 'true':
|
|
1221
|
+
from whatap.util.native_process import acquire_start_lock, configured_channel, find_existing_agent
|
|
1222
|
+
try:
|
|
1223
|
+
spec = _native_agent_spec({})
|
|
1224
|
+
with acquire_start_lock(spec):
|
|
1225
|
+
if find_existing_agent(spec):
|
|
1226
|
+
# CLI allocation must not rewrite a live bootstrap agent's channel.
|
|
1227
|
+
return configured_channel(spec.home, spec.environment, include_unix=False)[1]
|
|
1228
|
+
return _config_port()
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
# 락 타임아웃/다중 인스턴스 등은 CLI 를 죽이지 말고 기존 포트 할당으로 폴백
|
|
1231
|
+
whatap_print('WHATAP: live agent check skipped, falling back to port file: %s' % e)
|
|
1232
|
+
return _config_port()
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def _config_port():
|
|
969
1236
|
# force_net_udp_port가 설정된 경우 lock 파일 무시하고 해당 포트 강제 사용
|
|
970
1237
|
force_port_str = preview_whatap_conf("force_net_udp_port")
|
|
971
1238
|
if force_port_str and force_port_str not in ('false', 'False'):
|
|
Binary file
|
|
Binary file
|
whatap/build.py
CHANGED