whatap-python 2.1.0__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.
Files changed (227) hide show
  1. whatap/LICENSE +0 -0
  2. whatap/README.rst +49 -0
  3. whatap/__init__.py +923 -0
  4. whatap/__main__.py +4 -0
  5. whatap/agent/darwin/amd64/whatap_python +0 -0
  6. whatap/agent/darwin/arm64/whatap_python +0 -0
  7. whatap/agent/linux/amd64/whatap_python +0 -0
  8. whatap/agent/linux/arm64/whatap_python +0 -0
  9. whatap/agent/windows/whatap_python.exe +0 -0
  10. whatap/bootstrap/__init__.py +0 -0
  11. whatap/bootstrap/sitecustomize.py +19 -0
  12. whatap/build.py +4 -0
  13. whatap/conf/__init__.py +0 -0
  14. whatap/conf/configuration.py +280 -0
  15. whatap/conf/configure.py +105 -0
  16. whatap/conf/license.py +49 -0
  17. whatap/control/__init__.py +0 -0
  18. whatap/counter/__init__.py +14 -0
  19. whatap/counter/counter_manager.py +45 -0
  20. whatap/counter/tasks/__init__.py +3 -0
  21. whatap/counter/tasks/base_task.py +26 -0
  22. whatap/counter/tasks/llm_evaluator_task.py +501 -0
  23. whatap/counter/tasks/llm_log_sink_task.py +309 -0
  24. whatap/counter/tasks/llm_stat_task.py +78 -0
  25. whatap/counter/tasks/openfiledescriptor.py +67 -0
  26. whatap/io/__init__.py +1 -0
  27. whatap/io/data_inputx.py +161 -0
  28. whatap/io/data_outputx.py +262 -0
  29. whatap/llm/__init__.py +17 -0
  30. whatap/llm/definitions.py +43 -0
  31. whatap/llm/evaluators/__init__.py +136 -0
  32. whatap/llm/evaluators/base.py +114 -0
  33. whatap/llm/evaluators/builtins/__init__.py +91 -0
  34. whatap/llm/evaluators/builtins/answer_relevance.py +46 -0
  35. whatap/llm/evaluators/builtins/combined_judge.py +271 -0
  36. whatap/llm/evaluators/builtins/factuality.py +71 -0
  37. whatap/llm/evaluators/builtins/hallucination.py +97 -0
  38. whatap/llm/evaluators/builtins/llm_judge.py +516 -0
  39. whatap/llm/evaluators/builtins/pii_leak.py +214 -0
  40. whatap/llm/evaluators/builtins/prompt_injection.py +71 -0
  41. whatap/llm/evaluators/builtins/toxicity.py +53 -0
  42. whatap/llm/evaluators/builtins/url_scan.py +194 -0
  43. whatap/llm/evaluators/registry.py +192 -0
  44. whatap/llm/evaluators/sampler.py +83 -0
  45. whatap/llm/evaluators/scope.py +334 -0
  46. whatap/llm/features.py +66 -0
  47. whatap/llm/log_sink_packs/__init__.py +9 -0
  48. whatap/llm/log_sink_packs/llm_input_message.py +16 -0
  49. whatap/llm/log_sink_packs/llm_log_sink_pack.py +72 -0
  50. whatap/llm/log_sink_packs/llm_output_message.py +19 -0
  51. whatap/llm/log_sink_packs/llm_step_eval_status.py +94 -0
  52. whatap/llm/log_sink_packs/llm_step_status.py +118 -0
  53. whatap/llm/log_sink_packs/llm_system_message.py +16 -0
  54. whatap/llm/log_sink_packs/llm_tool_calls.py +44 -0
  55. whatap/llm/log_sink_packs/llm_tool_results.py +16 -0
  56. whatap/llm/log_sink_packs/llm_tx_status.py +108 -0
  57. whatap/llm/pricing.py +236 -0
  58. whatap/llm/prompt_meta.py +288 -0
  59. whatap/llm/providers/__init__.py +0 -0
  60. whatap/llm/providers/anthropic/__init__.py +37 -0
  61. whatap/llm/providers/anthropic/messages/__init__.py +0 -0
  62. whatap/llm/providers/anthropic/messages/messages.py +70 -0
  63. whatap/llm/providers/anthropic/messages/messages_context.py +76 -0
  64. whatap/llm/providers/anthropic/messages/messages_extractor.py +126 -0
  65. whatap/llm/providers/interceptor.py +182 -0
  66. whatap/llm/providers/openai/__init__.py +133 -0
  67. whatap/llm/providers/openai/chat/__init__.py +0 -0
  68. whatap/llm/providers/openai/chat/chat.py +82 -0
  69. whatap/llm/providers/openai/chat/chat_context.py +78 -0
  70. whatap/llm/providers/openai/chat/chat_extractor.py +127 -0
  71. whatap/llm/providers/openai/completions/__init__.py +0 -0
  72. whatap/llm/providers/openai/completions/completions.py +70 -0
  73. whatap/llm/providers/openai/completions/completions_context.py +31 -0
  74. whatap/llm/providers/openai/completions/completions_extractor.py +61 -0
  75. whatap/llm/providers/openai/content_parser.py +41 -0
  76. whatap/llm/providers/openai/embeddings/__init__.py +0 -0
  77. whatap/llm/providers/openai/embeddings/embeddings.py +59 -0
  78. whatap/llm/providers/openai/embeddings/embeddings_context.py +25 -0
  79. whatap/llm/providers/openai/embeddings/embeddings_extractor.py +26 -0
  80. whatap/llm/providers/openai/responses/__init__.py +0 -0
  81. whatap/llm/providers/openai/responses/responses.py +70 -0
  82. whatap/llm/providers/openai/responses/responses_context.py +88 -0
  83. whatap/llm/providers/openai/responses/responses_extractor.py +126 -0
  84. whatap/llm/providers/stream_accumulator.py +73 -0
  85. whatap/llm/stats/__init__.py +35 -0
  86. whatap/llm/stats/active_stat.py +86 -0
  87. whatap/llm/stats/answer_relevance_eval_stat.py +10 -0
  88. whatap/llm/stats/api_status_stat.py +35 -0
  89. whatap/llm/stats/base_stat.py +107 -0
  90. whatap/llm/stats/combined_judge_eval_stat.py +11 -0
  91. whatap/llm/stats/error_stat.py +59 -0
  92. whatap/llm/stats/eval_stat.py +225 -0
  93. whatap/llm/stats/factuality_eval_stat.py +10 -0
  94. whatap/llm/stats/feature_stat.py +104 -0
  95. whatap/llm/stats/finish_stat.py +105 -0
  96. whatap/llm/stats/hallucination_eval_stat.py +10 -0
  97. whatap/llm/stats/meter.py +18 -0
  98. whatap/llm/stats/perf_stat.py +117 -0
  99. whatap/llm/stats/pii_leak_eval_stat.py +12 -0
  100. whatap/llm/stats/prompt_injection_eval_stat.py +10 -0
  101. whatap/llm/stats/token_usage_stat.py +133 -0
  102. whatap/llm/stats/toxicity_eval_stat.py +10 -0
  103. whatap/llm/stats/url_scan_eval_stat.py +12 -0
  104. whatap/net/__init__.py +0 -0
  105. whatap/net/async_sender.py +107 -0
  106. whatap/net/packet_enum.py +44 -0
  107. whatap/net/packet_type_enum.py +31 -0
  108. whatap/net/param_def.py +69 -0
  109. whatap/net/stackhelper.py +87 -0
  110. whatap/net/udp_session.py +394 -0
  111. whatap/net/udp_thread.py +54 -0
  112. whatap/pack/__init__.py +0 -0
  113. whatap/pack/logSinkPack.py +77 -0
  114. whatap/pack/pack.py +34 -0
  115. whatap/pack/pack_enum.py +41 -0
  116. whatap/pack/tagCountPack.py +61 -0
  117. whatap/scripts/__init__.py +208 -0
  118. whatap/trace/__init__.py +12 -0
  119. whatap/trace/mod/__init__.py +0 -0
  120. whatap/trace/mod/amqp/__init__.py +0 -0
  121. whatap/trace/mod/amqp/kombu.py +122 -0
  122. whatap/trace/mod/amqp/pika.py +62 -0
  123. whatap/trace/mod/application/__init__.py +0 -0
  124. whatap/trace/mod/application/bottle.py +34 -0
  125. whatap/trace/mod/application/celery.py +81 -0
  126. whatap/trace/mod/application/cherrypy.py +30 -0
  127. whatap/trace/mod/application/django.py +287 -0
  128. whatap/trace/mod/application/django_asgi.py +266 -0
  129. whatap/trace/mod/application/django_py3.py +251 -0
  130. whatap/trace/mod/application/fastapi/__init__.py +31 -0
  131. whatap/trace/mod/application/fastapi/endpoint.py +73 -0
  132. whatap/trace/mod/application/fastapi/exception_log.py +63 -0
  133. whatap/trace/mod/application/fastapi/instrumentation.py +204 -0
  134. whatap/trace/mod/application/fastapi/scope.py +115 -0
  135. whatap/trace/mod/application/fastapi/transaction.py +67 -0
  136. whatap/trace/mod/application/flask.py +52 -0
  137. whatap/trace/mod/application/frappe.py +224 -0
  138. whatap/trace/mod/application/graphql.py +170 -0
  139. whatap/trace/mod/application/nameko.py +39 -0
  140. whatap/trace/mod/application/odoo.py +63 -0
  141. whatap/trace/mod/application/starlette.py +126 -0
  142. whatap/trace/mod/application/tornado.py +163 -0
  143. whatap/trace/mod/application/wsgi.py +195 -0
  144. whatap/trace/mod/database/__init__.py +0 -0
  145. whatap/trace/mod/database/cxoracle.py +49 -0
  146. whatap/trace/mod/database/mongo.py +169 -0
  147. whatap/trace/mod/database/mysql.py +80 -0
  148. whatap/trace/mod/database/neo4j.py +90 -0
  149. whatap/trace/mod/database/psycopg2.py +45 -0
  150. whatap/trace/mod/database/psycopg3.py +359 -0
  151. whatap/trace/mod/database/redis.py +122 -0
  152. whatap/trace/mod/database/sqlalchemy.py +213 -0
  153. whatap/trace/mod/database/sqlite3.py +130 -0
  154. whatap/trace/mod/database/util.py +630 -0
  155. whatap/trace/mod/email/__init__.py +0 -0
  156. whatap/trace/mod/email/smtp.py +78 -0
  157. whatap/trace/mod/httpc/__init__.py +0 -0
  158. whatap/trace/mod/httpc/django.py +31 -0
  159. whatap/trace/mod/httpc/httplib.py +70 -0
  160. whatap/trace/mod/httpc/httpx.py +62 -0
  161. whatap/trace/mod/httpc/requests.py +20 -0
  162. whatap/trace/mod/httpc/urllib3.py +27 -0
  163. whatap/trace/mod/httpc/util.py +388 -0
  164. whatap/trace/mod/logging.py +161 -0
  165. whatap/trace/mod/plugin.py +84 -0
  166. whatap/trace/mod/standalone/__init__.py +0 -0
  167. whatap/trace/mod/standalone/multiple.py +293 -0
  168. whatap/trace/mod/standalone/single.py +135 -0
  169. whatap/trace/simple_trace_context.py +18 -0
  170. whatap/trace/trace_context.py +212 -0
  171. whatap/trace/trace_context_manager.py +244 -0
  172. whatap/trace/trace_error.py +84 -0
  173. whatap/trace/trace_handler.py +89 -0
  174. whatap/trace/trace_import.py +91 -0
  175. whatap/trace/trace_module_definition.py +156 -0
  176. whatap/util/__init__.py +0 -0
  177. whatap/util/bit_util.py +49 -0
  178. whatap/util/cardinality/__init__.py +0 -0
  179. whatap/util/cardinality/hyperloglog.py +84 -0
  180. whatap/util/cardinality/murmurhash.py +20 -0
  181. whatap/util/cardinality/registerset.py +60 -0
  182. whatap/util/compare_util.py +19 -0
  183. whatap/util/date_util.py +55 -0
  184. whatap/util/debug_util.py +73 -0
  185. whatap/util/escape_literal_sql.py +233 -0
  186. whatap/util/frame_util.py +20 -0
  187. whatap/util/hash_util.py +103 -0
  188. whatap/util/hexa32.py +66 -0
  189. whatap/util/int_set.py +199 -0
  190. whatap/util/ip_util.py +63 -0
  191. whatap/util/keygen.py +11 -0
  192. whatap/util/linked_list.py +113 -0
  193. whatap/util/linked_map.py +359 -0
  194. whatap/util/metering_util.py +103 -0
  195. whatap/util/request_double_queue.py +68 -0
  196. whatap/util/request_queue.py +60 -0
  197. whatap/util/string_util.py +20 -0
  198. whatap/util/throttle_util.py +99 -0
  199. whatap/util/userid_util.py +134 -0
  200. whatap/value/__init__.py +1 -0
  201. whatap/value/blob_value.py +38 -0
  202. whatap/value/boolean_value.py +33 -0
  203. whatap/value/decimal_value.py +36 -0
  204. whatap/value/double_summary.py +86 -0
  205. whatap/value/double_value.py +33 -0
  206. whatap/value/float_array.py +42 -0
  207. whatap/value/float_value.py +34 -0
  208. whatap/value/int_array.py +42 -0
  209. whatap/value/ip4_value.py +50 -0
  210. whatap/value/list_value.py +105 -0
  211. whatap/value/long_array.py +44 -0
  212. whatap/value/long_summary.py +83 -0
  213. whatap/value/map_value.py +154 -0
  214. whatap/value/null_value.py +21 -0
  215. whatap/value/number_value.py +33 -0
  216. whatap/value/summary_value.py +39 -0
  217. whatap/value/text_array.py +58 -0
  218. whatap/value/text_hash_value.py +37 -0
  219. whatap/value/text_value.py +43 -0
  220. whatap/value/value.py +26 -0
  221. whatap/value/value_enum.py +80 -0
  222. whatap/whatap.conf +14 -0
  223. whatap_python-2.1.0.dist-info/METADATA +87 -0
  224. whatap_python-2.1.0.dist-info/RECORD +227 -0
  225. whatap_python-2.1.0.dist-info/WHEEL +5 -0
  226. whatap_python-2.1.0.dist-info/entry_points.txt +6 -0
  227. whatap_python-2.1.0.dist-info/top_level.txt +1 -0
whatap/__init__.py ADDED
@@ -0,0 +1,923 @@
1
+ import logging as logging_module
2
+ import os
3
+ import platform
4
+ import sys
5
+ import subprocess, signal
6
+ import time
7
+ from whatap import build
8
+ from whatap.util.date_util import DateUtil
9
+ import threading
10
+ import builtins
11
+
12
+ __version__ = build.version
13
+
14
+ LOGGING_MSG_FORMAT = '[%(asctime)s] : - %(message)s'
15
+ LOGGING_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
16
+
17
+ logging = logging_module.getLogger(__name__)
18
+
19
+ PY2 = sys.version_info[0] == 2
20
+ PY3 = sys.version_info[0] == 3
21
+
22
+ ROOT_DIR = __file__
23
+
24
+
25
+ class ContextFilter(logging_module.Filter):
26
+ def __init__(self):
27
+ super(ContextFilter, self).__init__()
28
+ self.last_id = None
29
+
30
+ def filter(self, record):
31
+ try:
32
+ if record.id:
33
+ if self.last_id == record.id:
34
+ return False
35
+
36
+ self.last_id = record.id
37
+ return True
38
+
39
+ except Exception as e:
40
+ record.id = ''
41
+ return True
42
+
43
+
44
+
45
+ from whatap.conf.configure import Configure as conf
46
+ CONFIG_FILE_NAME = 'whatap.conf'
47
+ LOG_FILE_NAME = 'whatap-hook.log'
48
+
49
+ isFrappeCommands = "get-frappe-commands" in sys.argv if hasattr(sys, "argv") else False
50
+
51
+ def preview_whatap_conf(option_name:str):
52
+ home = os.environ.get('WHATAP_HOME', '.')
53
+ whatap_config = os.path.join(home, 'whatap.conf')
54
+
55
+
56
+ """
57
+ 현재 preview_whatap_conf 를 사용중인 옵션
58
+ - ignore_whatap_stdout (False)
59
+ - standalone_enabled (False)
60
+ - counter_thread_enabled (False)
61
+ """
62
+ value = None
63
+ try:
64
+ with open(whatap_config) as f:
65
+ for raw in f:
66
+ line = raw.strip()
67
+ if not line or line.startswith('#'):
68
+ continue
69
+ if line.startswith(option_name):
70
+ parts = line.split('=', 1)
71
+ if len(parts) == 2:
72
+ value = parts[1].strip()
73
+ break
74
+
75
+ except FileNotFoundError:
76
+ pass
77
+
78
+ except Exception as e:
79
+ print(f'WHATAP: config parse error ({e!r})')
80
+
81
+ if value is not None:
82
+ return value
83
+
84
+ env_value = os.environ.get(option_name)
85
+ if env_value is not None:
86
+ return env_value
87
+
88
+ return 'false'
89
+
90
+
91
+
92
+ ignore_whatap_stdout = preview_whatap_conf("ignore_whatap_stdout")
93
+
94
+
95
+
96
+ original_print = builtins.print
97
+
98
+ def print_option(func):
99
+ def wrapper(*args, **kwargs):
100
+
101
+ if(ignore_whatap_stdout == 'false'):
102
+ result = func(*args, **kwargs)
103
+ else:
104
+ result = None
105
+ return result
106
+ return wrapper
107
+
108
+ __builtins__ = dict(__builtins__)
109
+ __builtins__['print'] = print_option(original_print)
110
+
111
+
112
+ def whatap_print(*args):
113
+ if isFrappeCommands:
114
+ logging.info(*args)
115
+ else:
116
+ if len(args) > 0:
117
+ message = " ".join(args)
118
+ print(message)
119
+
120
+ class Logger(object):
121
+ def __init__(self):
122
+ self.logger = logging
123
+ self.logger.addFilter(ContextFilter())
124
+ self.handler = None
125
+
126
+ self.create_log()
127
+
128
+ def create_log(self):
129
+ os.environ['WHATAP_LOGS'] = os.path.join(os.environ['WHATAP_HOME'],
130
+ 'logs')
131
+ if not os.path.exists(os.environ['WHATAP_LOGS']):
132
+ try:
133
+ os.mkdir(os.environ['WHATAP_LOGS'])
134
+
135
+ except Exception as e:
136
+ whatap_print('WHATAP: LOG FILE WRITE ERROR.')
137
+ whatap_print(
138
+ 'WHATAP: Try to execute command. \n {}'.format(
139
+ 'sudo mkdir -m 777 -p $WHATAP_HOME/logs`'))
140
+
141
+ self.print_log()
142
+
143
+ def print_log(self):
144
+ try:
145
+ if self.handler:
146
+ self.logger.removeHandler(self.handler)
147
+
148
+ temp_logging_msg_format = '[%(asctime)s] : %(id)s - %(message)s'
149
+ logging_format = logging_module.Formatter(
150
+ fmt=temp_logging_msg_format, datefmt=LOGGING_DATE_FORMAT)
151
+
152
+ fh = logging_module.FileHandler(
153
+ os.path.join(os.environ['WHATAP_LOGS'], LOG_FILE_NAME))
154
+ fh.setFormatter(logging_format)
155
+ self.logger.addHandler(fh)
156
+ self.handler = fh
157
+
158
+ self.logger.setLevel(logging_module.DEBUG)
159
+ except Exception as e:
160
+ whatap_print('WHATAP: LOGGING ERROR: {}'.format(e))
161
+ else:
162
+ self.print_whatap()
163
+
164
+ def print_whatap(self):
165
+ str = '\n' + \
166
+ ' _ ____ ______' + build.app + '-AGENT \n' + \
167
+ '| | /| / / / ___ /_ __/__ ____' + '\n' + \
168
+ '| |/ |/ / _ \\/ _ `// / / _ `/ _ \\' + '\n' + \
169
+ '|__/|__/_//_/\\_,_//_/ \\_,_/ .__/' + '\n' + \
170
+ ' /_/' + '\n' + \
171
+ 'Just Tap, Always Monitoring' + '\n' + \
172
+ 'WhaTap ' + build.app + ' Agent version ' + build.version + ', ' + build.release_date + '\n\n'
173
+
174
+ str += '{0}: {1}\n'.format('WHATAP_HOME', os.environ['WHATAP_HOME'])
175
+ str += '{0}: {1}\n'.format('Config', os.path.join(os.environ['WHATAP_HOME'],
176
+ os.environ['WHATAP_CONFIG']))
177
+ str += '{0}: {1}\n\n'.format('Logs', os.environ['WHATAP_LOGS'])
178
+
179
+ whatap_print(str)
180
+ logging.debug(str)
181
+
182
+
183
+ def read_file(home, file_name):
184
+ data = ''
185
+ try:
186
+ f = open(os.path.join(os.environ.get(home), file_name), 'r+')
187
+ data = str(f.readline()).strip()
188
+ f.close()
189
+ finally:
190
+ return data
191
+
192
+
193
+ def write_file(home, file_name, value):
194
+ try:
195
+ f = open(os.path.join(os.environ.get(home), file_name), 'w+')
196
+ f.write(value)
197
+ f.close()
198
+ except Exception as e:
199
+ whatap_print('WHATAP: WHATAP HOME ERROR. (path: {})'.format(os.path.join(os.environ.get(home))))
200
+ whatap_print(
201
+ 'WHATAP: Try to execute command. \n {}'.format(
202
+ '`sudo chmod -R 777 $WHATAP_HOME`'))
203
+ return False
204
+ else:
205
+ return True
206
+
207
+
208
+ def check_whatap_home(target='WHATAP_HOME'):
209
+ whatap_home = os.environ.get(target)
210
+ if not whatap_home:
211
+ whatap_home = find_whatap_conf()
212
+ if not whatap_home:
213
+ whatap_print('WHATAP: ${} is empty'.format(target))
214
+
215
+ return whatap_home
216
+
217
+
218
+ def init_config(home):
219
+ whatap_home = os.environ.get(home)
220
+ if not whatap_home:
221
+ whatap_home = find_whatap_conf()
222
+ if not whatap_home:
223
+ whatap_home = read_file(home, home.lower())
224
+ if not whatap_home:
225
+ whatap_home = os.getcwd()
226
+ os.environ[home] = whatap_home
227
+
228
+ whatap_print('WHATAP: WHATAP_HOME is empty')
229
+ whatap_print(
230
+ 'WHATAP: WHATAP_HOME set default CURRENT_WORKING_DIRECTORY value')
231
+ whatap_print('CURRENT_WORKING_DIRECTORY is {}\n'.format(whatap_home))
232
+
233
+ if not write_file(home, home.lower(), whatap_home):
234
+ return False
235
+
236
+ os.environ[home] = whatap_home
237
+ config_file = os.path.join(os.environ[home],
238
+ CONFIG_FILE_NAME)
239
+
240
+ if not os.path.exists(config_file):
241
+ with open(
242
+ os.path.join(os.path.dirname(__file__),
243
+ CONFIG_FILE_NAME),
244
+ 'r') as f:
245
+ content = f.read()
246
+ try:
247
+ with open(config_file, 'w+') as new_f:
248
+ new_f.write(content)
249
+ except Exception as e:
250
+ whatap_print('WHATAP: PERMISSION ERROR: {}'.format(e))
251
+ whatap_print(
252
+ 'WHATAP: Try to execute command. \n {}'.format(
253
+ '`sudo chmod -R 777 $WHATAP_HOME`'))
254
+ return False
255
+
256
+ return True
257
+
258
+
259
+ def update_config(home, opt_key, opt_value):
260
+ config_file = os.path.join(os.environ[home],
261
+ CONFIG_FILE_NAME)
262
+ try:
263
+ with open(config_file, 'r+') as f:
264
+ is_update = False
265
+ content = ''
266
+ for line in f:
267
+ if line:
268
+ key = line.split('=')[0].strip()
269
+ if key == opt_key:
270
+ is_update = True
271
+ line = '{0}={1}\n'.format(key, opt_value)
272
+
273
+ content += line
274
+ if not is_update:
275
+ content += '\n{0}={1}\n'.format(opt_key, opt_value)
276
+ open(config_file, 'w+').write(content)
277
+
278
+ except Exception as e:
279
+ whatap_print('WHATAP: OPTION ERROR: {}'.format(e))
280
+
281
+
282
+ def config(home):
283
+ os.environ['WHATAP_CONFIG'] = CONFIG_FILE_NAME
284
+
285
+ from whatap.conf.configure import Configure as conf
286
+ if conf.init():
287
+ from whatap.net.packet_enum import PacketEnum
288
+ PacketEnum.PORT = int(conf.net_udp_port)
289
+
290
+ if getattr(conf, 'apm_enabled', False):
291
+ from whatap.conf.license import License
292
+ conf.PCODE = License.getProjectCode(conf.license)
293
+
294
+ import whatap.counter
295
+
296
+ hooks(home)
297
+
298
+
299
+ from whatap.trace.trace_import import ImportFinder
300
+ from whatap.trace.trace_module_definition import DEFINITION, IMPORT_HOOKS, \
301
+ PLUGIN
302
+
303
+
304
+ def _build_ignore_instrumentation_set():
305
+ """whatap.conf 의 ignore_instrumentation_set 값을 정규화하여 반환.
306
+ Configure.load() 가 _set 접미사 키를 콤마 split 해 list 로 만들지만,
307
+ 환경변수/직접 setProperty 등 다른 경로로 들어온 경우(str)도 함께 처리한다.
308
+ """
309
+ raw = getattr(conf, 'ignore_instrumentation_set', None) or []
310
+ if isinstance(raw, str):
311
+ raw = raw.split(',')
312
+ return {str(item).strip().lower() for item in raw if item and str(item).strip()}
313
+
314
+
315
+ def _is_instrumentation_ignored(definition_key, ignore_set):
316
+ """DEFINITION 키(예: 'database.redis', 'logging')가 무시 대상인지 판정.
317
+
318
+ 매칭 규칙(셋 중 하나라도 맞으면 True):
319
+ 1) 키 전체가 일치 예: 'database.redis' == 'database.redis'
320
+ 2) 카테고리(점 앞)와 일치 예: 'database' → 'database.*' 모두
321
+ 3) 라이브러리(점 뒤)와 일치 예: 'redis' → 'database.redis'
322
+ """
323
+ if not ignore_set:
324
+ return False
325
+ key_lower = definition_key.lower()
326
+ if key_lower in ignore_set:
327
+ return True
328
+ if '.' in key_lower:
329
+ category, lib = key_lower.split('.', 1)
330
+ if category in ignore_set or lib in ignore_set:
331
+ return True
332
+ return False
333
+
334
+
335
+ def hooks(home):
336
+ ignore_set = _build_ignore_instrumentation_set()
337
+ skipped_keys = []
338
+ try:
339
+ for key, value_list in DEFINITION.items():
340
+ if _is_instrumentation_ignored(key, ignore_set):
341
+ skipped_keys.append(key)
342
+ continue
343
+ for value in value_list:
344
+ if len(value) >= 3 and isinstance(value[2], str):
345
+ module_path = value[2]
346
+ elif len(value) == 3 and not value[2]:
347
+ continue
348
+ else:
349
+ module_path = '{0}.{1}.{2}.{3}'.format(
350
+ 'whatap', 'trace', 'mod', key)
351
+ IMPORT_HOOKS[value[0]] = {'def': value[1],
352
+ 'module': module_path}
353
+ except Exception as e:
354
+ logging.debug(e, extra={'id': 'MODULE ERROR'})
355
+ finally:
356
+ try:
357
+ if skipped_keys:
358
+ logging.debug(
359
+ 'WHATAP: ignore_instrumentation_set skipped: {}'.format(
360
+ ','.join(skipped_keys)),
361
+ extra={'id': 'WA_IGN'})
362
+
363
+ if conf.trace_logging_enabled and not _is_instrumentation_ignored(
364
+ 'logging', ignore_set):
365
+ logging_module = sys.modules.get("logging")
366
+ from whatap.trace.mod.logging import instrument_logging
367
+ instrument_logging(logging_module)
368
+
369
+ if conf.hook_method_patterns:
370
+ from whatap.trace.mod.plugin import instrument_plugin
371
+ patterns = conf.hook_method_patterns.split(',')
372
+ for pattern in patterns:
373
+ pattern=pattern.strip()
374
+
375
+ module_name, class_def = pattern.split(':')
376
+ if not PLUGIN.get(module_name):
377
+ PLUGIN[module_name] = []
378
+ PLUGIN[module_name].append(class_def)
379
+
380
+ DEFINITION["plugin"].append(
381
+ (module_name, 'instrument_plugin'))
382
+
383
+ key = 'plugin'
384
+ for value in DEFINITION[key]:
385
+ IMPORT_HOOKS[value[0]] = {'def': value[1],
386
+ 'module': '{0}.{1}.{2}.{3}'.format(
387
+ 'whatap',
388
+ 'trace',
389
+ 'mod',
390
+ key)}
391
+
392
+ if conf.standalone_enabled:
393
+ if conf.standalone_type == 'multiple-transaction':
394
+ from whatap.trace.mod.standalone.multiple import instrument_standalone_multiple
395
+ instrument_standalone_multiple()
396
+ else:
397
+ from whatap.trace.mod.standalone.single import instrument_standalone_single
398
+ instrument_standalone_single()
399
+
400
+ except Exception as e:
401
+ logging.debug(e, extra={'id': 'PLUGIN ERROR'})
402
+ finally:
403
+ sys.meta_path.insert(0, ImportFinder())
404
+ logging.debug('WHATAP AGENT START!', extra={'id': 'WA000'})
405
+
406
+
407
+ def agent():
408
+ home = 'WHATAP_HOME'
409
+ whatap_home = os.environ.get(home)
410
+ if not whatap_home:
411
+ whatap_home = read_file(home, home.lower())
412
+ if not whatap_home:
413
+ whatap_home = os.getcwd()
414
+ os.environ[home] = whatap_home
415
+
416
+ whatap_print('WHATAP: WHATAP_HOME is empty')
417
+ whatap_print(
418
+ 'WHATAP: WHATAP_HOME set default CURRENT_WORKING_DIRECTORY value')
419
+ whatap_print('CURRENT_WORKING_DIRECTORY is {}\n'.format(whatap_home))
420
+
421
+ if write_file(home, home.lower(), whatap_home):
422
+ os.environ['WHATAP_HOME'] = whatap_home
423
+
424
+ whatap_code_start = preview_whatap_conf("whatap_code_start")
425
+ if whatap_code_start == 'true':
426
+ apm_enabled = preview_whatap_conf("apm_enabled")
427
+ llm_enabled = preview_whatap_conf("llm_enabled")
428
+
429
+ if apm_enabled == 'true':
430
+ t = threading.Thread(target=go)
431
+ t.setDaemon(True)
432
+ t.start()
433
+
434
+ if llm_enabled == 'true':
435
+ t = threading.Thread(target=go, kwargs={'llm': True})
436
+ t.setDaemon(True)
437
+ t.start()
438
+
439
+ config(home)
440
+
441
+ ARCH = {
442
+ 'x86_64': 'amd64',
443
+ 'x86': '386',
444
+ 'x86_32': '386',
445
+ 'ARM': 'arm',
446
+ 'AArch64': 'arm64',
447
+ 'arm64': 'arm64',
448
+ 'aarch64': 'arm64'
449
+ }
450
+
451
+ AGENT_NAME = 'whatap_python'
452
+
453
+
454
+
455
+ def go(batch=False, opts={}, llm=False):
456
+ newenv=os.environ.copy()
457
+ newenv['WHATAP_VERSION'] = build.version
458
+ newenv['whatap.start'] = str(DateUtil.now())
459
+ newenv['python.uptime'] = str(DateUtil.datetime())
460
+ newenv['python.version'] = sys.version
461
+ newenv['python.tzname'] = time.tzname[0]
462
+ newenv['os.release'] = platform.release()
463
+ newenv['sys.version_info'] = str(sys.version_info)
464
+ newenv['sys.executable'] = sys.executable
465
+ newenv['sys.path'] = str(sys.path)
466
+ newenv.update(opts)
467
+
468
+ if llm:
469
+ home = 'WHATAP_HOME'
470
+ file_name = AGENT_NAME + '.pid.llm'
471
+ elif not batch:
472
+ home = 'WHATAP_HOME'
473
+ file_name = AGENT_NAME + '.pid'
474
+ else:
475
+ home = 'WHATAP_HOME_BATCH'
476
+ file_name = AGENT_NAME + '.pid.batch'
477
+
478
+ def get_pname(pid):
479
+ # Linux/Unix: use /proc filesystem
480
+ if sys.platform != 'win32':
481
+ cmdlinepath = os.path.join('/proc', str(pid), 'cmdline')
482
+ if os.path.exists(cmdlinepath):
483
+ with open(cmdlinepath) as f:
484
+ content = f.read()
485
+ if content:
486
+ return content.strip()
487
+ else:
488
+ # Windows: use psutil to get process name
489
+ try:
490
+ import psutil
491
+ process = psutil.Process(int(pid))
492
+ return ' '.join(process.cmdline())
493
+ except Exception as e:
494
+ whatap_print('WHATAP: get_pname psutil failed: %s' % e)
495
+ return ''
496
+
497
+ pid = read_file(home, file_name)
498
+ if pid and get_pname(pid).find('whatap_python') >= 0:
499
+ if sys.platform == 'win32':
500
+ # Windows: Don't kill, just skip
501
+ whatap_print('WHATAP: Agent already running (PID: {}). Skipping duplicate execution.'.format(pid))
502
+ return
503
+ else:
504
+ # Unix/Linux: kill and replace existing process
505
+ try:
506
+ import signal
507
+ os.kill(int(pid), signal.SIGKILL)
508
+ except Exception as e:
509
+ whatap_print('WHATAP: SIGKILL failed for pid %s: %s' % (pid, e))
510
+
511
+ try:
512
+ home_path= os.environ.get(home)
513
+
514
+ # Windows uses .exe extension
515
+ agent_binary_name = AGENT_NAME + '.exe' if sys.platform == 'win32' else AGENT_NAME
516
+ dest_agent = os.path.join(home_path, agent_binary_name)
517
+
518
+ # Try to remove existing binary
519
+ # Windows: File may still be locked even after waiting, ignore error and use existing binary
520
+ # Unix/Linux: Removal should always succeed
521
+ needs_deployment = False
522
+ if os.path.exists(dest_agent):
523
+ try:
524
+ os.remove(dest_agent)
525
+ needs_deployment = True
526
+ except (OSError, PermissionError) as e:
527
+ if sys.platform == 'win32':
528
+ # Windows: File is still locked, use existing binary
529
+ pass
530
+ else:
531
+ # Unix/Linux: This should not happen
532
+ raise
533
+ else:
534
+ needs_deployment = True
535
+
536
+ # Windows doesn't use architecture subdirectories (only single x64 binary for now)
537
+ if sys.platform == 'win32':
538
+ source_cwd = os.path.join(os.path.dirname(__file__), 'agent', 'windows')
539
+ else:
540
+ source_cwd = os.path.join(os.path.join(os.path.dirname(__file__), 'agent'), platform.system().lower(),
541
+ ARCH[platform.machine()])
542
+
543
+ source_agent = os.path.join(source_cwd, agent_binary_name)
544
+
545
+ # Deploy agent binary only if needed
546
+ if needs_deployment:
547
+ # Try symlink first (Unix/Linux or Windows with proper permissions)
548
+ try:
549
+ os.symlink(source_agent, dest_agent)
550
+ except (OSError, NotImplementedError):
551
+ # Fallback to copy on Windows or when symlink fails
552
+ import shutil
553
+ shutil.copy2(source_agent, dest_agent)
554
+ # Make executable on Unix-like systems
555
+ if sys.platform != 'win32':
556
+ os.chmod(dest_agent, 0o755)
557
+
558
+ sockfile_path = os.path.join(home_path, 'run')
559
+ if not os.path.exists(sockfile_path):
560
+ os.mkdir(sockfile_path)
561
+ newenv['whatap.enabled'] = 'True'
562
+ newenv['WHATAP_PID_FILE'] = file_name
563
+ newenv['PYTHON_PARENT_APP_PID'] = str(os.getpid())
564
+
565
+ # Windows uses absolute path, Unix/Linux uses relative path
566
+ if sys.platform == 'win32':
567
+ agent_cmd = os.path.join(home_path, agent_binary_name)
568
+ else:
569
+ agent_cmd = './{0}'.format(agent_binary_name)
570
+
571
+ # Windows: Run in background with DETACHED_PROCESS
572
+ # Unix/Linux: Run in background with start_new_session
573
+ # 실행 인자 구성
574
+ cmd_args = [agent_cmd, '-t', '3']
575
+ if llm:
576
+ cmd_args.append('--llm')
577
+
578
+ label = 'LLM golang module' if llm else 'golang module'
579
+
580
+ if sys.platform == 'win32':
581
+ # SESSIONNAME 환경변수는 NSSM이 부모 세션에서 상속받아 부정확함
582
+ # ProcessIdToSessionId로 실제 프로세스 세션 ID 확인
583
+ try:
584
+ import ctypes
585
+ _sid = ctypes.c_ulong(0)
586
+ ctypes.windll.kernel32.ProcessIdToSessionId(
587
+ ctypes.windll.kernel32.GetCurrentProcessId(),
588
+ ctypes.byref(_sid)
589
+ )
590
+ is_session0 = (_sid.value == 0)
591
+ except Exception:
592
+ is_session0 = not os.environ.get('SESSIONNAME')
593
+
594
+ cmd_args.append('foreground')
595
+
596
+ if is_session0:
597
+ # Session 0에서만 WHATAP_FOREGROUND=1 추가 주입.
598
+ # IsAnInteractiveSession()이 false로 떨어지는 경로에서
599
+ # foreground CLI 인자를 보강하는 안전망.
600
+ newenv['WHATAP_FOREGROUND'] = '1'
601
+
602
+ # Session 0에서는 PIPE를 읽지 않으면 버퍼 고갈로 Go 프로세스가 블로킹됨
603
+ # 로그 파일로 리디렉션
604
+ # CREATE_NO_WINDOW: Session 0 서비스 컨텍스트에 적합한 플래그
605
+ CREATE_NO_WINDOW = 0x08000000
606
+ log_dir = os.path.join(home_path, 'logs')
607
+ go_log_path = os.path.join(log_dir, AGENT_NAME + '.log')
608
+ try:
609
+ go_out = open(go_log_path, 'ab')
610
+ except Exception:
611
+ go_out = subprocess.DEVNULL
612
+ process = subprocess.Popen(cmd_args,
613
+ cwd=home_path, env=newenv,
614
+ creationflags=CREATE_NO_WINDOW,
615
+ stdout=go_out, stderr=go_out)
616
+ else:
617
+ DETACHED_PROCESS = 0x00000008
618
+ process = subprocess.Popen(cmd_args,
619
+ cwd=home_path, env=newenv,
620
+ creationflags=DETACHED_PROCESS,
621
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
622
+ else:
623
+ process = subprocess.Popen(cmd_args,
624
+ cwd=home_path, env=newenv,
625
+ start_new_session=True,
626
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
627
+
628
+ time.sleep(0.5)
629
+ if process.poll() is not None:
630
+ if sys.platform == 'win32' and is_session0:
631
+ whatap_print("executed {} (exit code: {}, log: {})".format(label, process.returncode, go_log_path))
632
+ else:
633
+ stdouts, errs = process.communicate()
634
+ whatap_print("executed {} ".format(label), str(stdouts,"utf8"), str(errs, "utf8"))
635
+ else:
636
+ write_file(home, file_name, str(process.pid))
637
+ whatap_print("executed {} in background (PID: {})".format(label, process.pid))
638
+ except Exception as e:
639
+ import traceback
640
+ traceback.print_exc()
641
+ whatap_print('WHATAP: AGENT ERROR: {}'.format(e))
642
+ else:
643
+ whatap_print('WHATAP: AGENT UP! (process name: {}{})\n'.format(AGENT_NAME, ' --llm' if llm else ''))
644
+
645
+
646
+ import signal
647
+
648
+ from whatap.trace.trace_handler import trace_handler
649
+ from whatap.trace.trace_error import interceptor_step_error
650
+ from whatap.trace.mod.application.wsgi import interceptor, start_interceptor, \
651
+ end_interceptor
652
+ from whatap.trace.mod.application.fastapi import interceptor_error_log
653
+ from whatap.trace.trace_context import TraceContext, TraceContextManager
654
+
655
+ def register_app(fn):
656
+ @trace_handler(fn, True)
657
+ def trace(*args, **kwargs):
658
+ callback = None
659
+ try:
660
+ environ = args[0]
661
+ callback = interceptor((fn, environ), *args, **kwargs)
662
+ except Exception as e:
663
+ logging.debug('WHATAP(@register_app): ' + str(e),
664
+ extra={'id': 'WA777'}, exc_info=True)
665
+ finally:
666
+ return callback if callback else fn(*args, **kwargs)
667
+
668
+ if not os.environ.get('whatap.enabled'):
669
+ agent()
670
+
671
+ return trace
672
+
673
+
674
+ def method_profiling(fn):
675
+ def trace(*args, **kwargs):
676
+ callback = None
677
+ try:
678
+ ctx = TraceContext()
679
+ ctx.service_name=fn.__name__
680
+ start_interceptor(ctx)
681
+ callback = fn(*args, **kwargs)
682
+ except Exception as e:
683
+ ctx = TraceContextManager.getLocalContext()
684
+ interceptor_step_error(e, ctx=ctx)
685
+ interceptor_error_log(ctx.id, e, fn, args, kwargs)
686
+ logging.debug('WHATAP(@method_profiling): ' + str(e),
687
+ extra={'id': 'WA776'}, exc_info=True)
688
+ finally:
689
+ ctx = TraceContextManager.getLocalContext()
690
+ end_interceptor(ctx=ctx)
691
+ return callback
692
+
693
+ if not os.environ.get('whatap.enabled'):
694
+ agent()
695
+
696
+ return trace
697
+
698
+
699
+ def batch_agent():
700
+ home = 'WHATAP_HOME_BATCH'
701
+ batch_home = os.environ.get(home)
702
+ if not batch_home:
703
+ if not read_file(home, home.lower()):
704
+ whatap_print('WHATAP: WHATAP_HOME_BATCH is empty')
705
+ return
706
+
707
+ if write_file(home, home.lower(), batch_home):
708
+ os.environ['WHATAP_HOME_BATCH'] = batch_home
709
+ os.environ['WHATAP_HOME'] = batch_home
710
+ go(batch=True)
711
+
712
+
713
+ def batch_profiling(fn):
714
+ import inspect
715
+ frame = inspect.stack()[1]
716
+ module = inspect.getmodule(frame[0])
717
+
718
+ def trace(*args, **kwargs):
719
+ if not os.environ.get('whatap.batch.enabled'):
720
+ home = 'WHATAP_HOME_BATCH'
721
+ batch_home = read_file(home, home.lower())
722
+ if not batch_home:
723
+ whatap_print(
724
+ 'WHATAP(@batch_profiling): try, whatap-start-batch')
725
+ return fn(*args, **kwargs)
726
+ else:
727
+ os.environ['whatap.batch.enabled'] = 'True'
728
+ os.environ['WHATAP_HOME_BATCH'] = batch_home
729
+ os.environ['WHATAP_HOME'] = batch_home
730
+ config(home)
731
+
732
+ callback = None
733
+ try:
734
+ ctx = TraceContext()
735
+ ctx.service_name=os.path.basename(module.__file__)
736
+ ctx = start_interceptor(ctx)
737
+
738
+ callback = fn(*args, **kwargs)
739
+ end_interceptor(thread_id=ctx.thread_id)
740
+ except Exception as e:
741
+ logging.debug('WHATAP(@batch_profiling): ' + str(e),
742
+ extra={'id': 'WA777'}, exc_info=True)
743
+ finally:
744
+ return callback if callback else fn(*args, **kwargs)
745
+
746
+ return trace
747
+
748
+
749
+ import os, time
750
+ import errno
751
+ import sys
752
+
753
+ try:
754
+ import fcntl
755
+ HAS_FCNTL = True
756
+ except ImportError:
757
+ # fcntl is not available on Windows
758
+ HAS_FCNTL = False
759
+ if sys.platform == 'win32':
760
+ try:
761
+ import msvcrt
762
+ HAS_MSVCRT = True
763
+ except ImportError:
764
+ HAS_MSVCRT = False
765
+ else:
766
+ HAS_MSVCRT = False
767
+
768
+ # Windows uses different default temp path
769
+ if sys.platform == 'win32':
770
+ import tempfile
771
+ default_lock_file = os.path.join(tempfile.gettempdir(), 'whatap-python.lock')
772
+ default_llm_lock_file = os.path.join(tempfile.gettempdir(), 'whatap-python-llm.lock')
773
+ else:
774
+ default_lock_file = '/tmp/whatap-python.lock'
775
+ default_llm_lock_file = '/tmp/whatap-python-llm.lock'
776
+
777
+ def openPortFile(filepath=os.environ.get('WHATAP_LOCK_FILE', default_lock_file)):
778
+ f = None
779
+ i=0
780
+ while f == None and i < 100:
781
+ try:
782
+ f = open(filepath, 'r+')
783
+ except IOError as e:
784
+ if e.errno == 2:
785
+ prefix = os.path.split(filepath)[0]
786
+ try:
787
+ if not os.path.exists(prefix):
788
+ os.makedirs(prefix)
789
+ f = open(filepath, 'w+')
790
+ except Exception as e:
791
+ whatap_print('WHATAP: openLockFile makedirs/open failed: %s' % e)
792
+ i += 1
793
+
794
+ if f:
795
+ try:
796
+ if HAS_FCNTL:
797
+ fcntl.lockf(f, fcntl.LOCK_EX)
798
+ elif HAS_MSVCRT:
799
+ # Windows file locking using msvcrt
800
+ try:
801
+ # Use non-blocking lock to avoid OSError
802
+ msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
803
+ except OSError:
804
+ # File already locked by another process, skip locking
805
+ pass
806
+ # If no locking mechanism available, proceed without lock
807
+ return f
808
+ except Exception as e:
809
+ whatap_print(e)
810
+ try:
811
+ f.close()
812
+ except Exception as e:
813
+ whatap_print('WHATAP: file close failed: %s' % e)
814
+ return None
815
+
816
+ def get_port_number(port=6600, home=os.environ.get('WHATAP_HOME'), lock_file=None):
817
+ if not home:
818
+ return None
819
+
820
+ for i in range(100):
821
+ f = openPortFile(filepath=lock_file) if lock_file else openPortFile()
822
+ if not f:
823
+ if i > 50:
824
+ time.sleep(0.1)
825
+ continue
826
+ if f:
827
+ lastPortFound = None
828
+ for l in f.readlines():
829
+ l = l.strip()
830
+ try:
831
+ (portFound, portHome) = l.split()
832
+ portFound = int(portFound)
833
+ except Exception as e:
834
+ whatap_print('WHATAP: port file parse failed: %s' % e)
835
+ continue
836
+ if home == portHome:
837
+ return portFound
838
+ if not lastPortFound or lastPortFound < portFound:
839
+ lastPortFound = int(portFound)
840
+ if not lastPortFound:
841
+ lastPortFound = port
842
+ else:
843
+ lastPortFound += 1
844
+ f.write(str(lastPortFound))
845
+ f.write('\t')
846
+ f.write(home)
847
+ f.write('\n')
848
+ try:
849
+ if HAS_FCNTL:
850
+ fcntl.lockf(f, fcntl.LOCK_UN)
851
+ elif HAS_MSVCRT:
852
+ # Windows file unlocking using msvcrt
853
+ try:
854
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
855
+ except OSError as e:
856
+ whatap_print('WHATAP: msvcrt file unlock failed: %s' % e)
857
+ except Exception as e:
858
+ whatap_print('WHATAP: file unlock failed: %s' % e)
859
+ f.close()
860
+ return lastPortFound
861
+
862
+ return port
863
+
864
+
865
+ def configPort():
866
+ # force_net_udp_port가 설정된 경우 lock 파일 무시하고 해당 포트 강제 사용
867
+ force_port_str = preview_whatap_conf("force_net_udp_port")
868
+ if force_port_str and force_port_str not in ('false', 'False'):
869
+ try:
870
+ port = int(force_port_str)
871
+ update_config('WHATAP_HOME', 'net_udp_port', str(port))
872
+ return port
873
+ except ValueError:
874
+ whatap_print('WHATAP: force_net_udp_port value is invalid: {}'.format(force_port_str))
875
+ else:
876
+ port = get_port_number()
877
+ if port:
878
+ update_config('WHATAP_HOME', 'net_udp_port', str(port))
879
+ return port
880
+
881
+
882
+ def configLlmPort():
883
+ # force_llm_net_udp_port가 설정된 경우 lock 파일 무시하고 해당 포트 강제 사용
884
+ force_port_str = preview_whatap_conf("force_llm_net_udp_port")
885
+ if force_port_str and force_port_str not in ('false', 'False'):
886
+ try:
887
+ port = int(force_port_str)
888
+ except ValueError:
889
+ whatap_print('WHATAP: force_llm_net_udp_port value is invalid: {}'.format(force_port_str))
890
+ port = None
891
+ else:
892
+ llm_lock_file = os.environ.get('WHATAP_LLM_LOCK_FILE', default_llm_lock_file)
893
+ port = get_port_number(port=6700, home=os.environ.get('WHATAP_HOME'), lock_file=llm_lock_file)
894
+
895
+ if port:
896
+ update_config('WHATAP_HOME', 'llm_net_udp_port', str(port))
897
+ return port
898
+
899
+
900
+ def find_whatap_conf():
901
+ # 1. 현재 디렉토리 검색
902
+ script_dir = os.path.dirname(os.path.abspath(__file__))
903
+ parent_dir = os.path.dirname(script_dir)
904
+ conf_path = os.path.join(parent_dir, 'whatap.conf')
905
+ if os.path.exists(conf_path):
906
+ os.environ['WHATAP_HOME'] = parent_dir
907
+ return conf_path
908
+
909
+ # 2. 상위 디렉토리들 검색
910
+ current = parent_dir
911
+ # Cross-platform root detection: keep going until dirname doesn't change
912
+ while True:
913
+ conf_path = os.path.join(current, 'whatap.conf')
914
+ if os.path.exists(conf_path):
915
+ os.environ['WHATAP_HOME'] = current
916
+ return conf_path
917
+ parent = os.path.dirname(current)
918
+ # Stop at root (Unix: '/', Windows: 'C:\' or similar)
919
+ if parent == current:
920
+ break
921
+ current = parent
922
+
923
+ return None