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
@@ -0,0 +1,251 @@
1
+
2
+ from whatap.conf.configure import Configure as conf
3
+ from whatap.trace.trace_context_manager import TraceContextManager
4
+ from whatap.trace.trace_context import TraceContext
5
+ from whatap.trace.trace_error import interceptor_error
6
+ from whatap.trace.mod.application.wsgi import \
7
+ isIgnore, start_interceptor, end_interceptor
8
+ from whatap.util.hash_util import HashUtil as hash_util
9
+ from whatap.util.userid_util import UseridUtil
10
+ from whatap.util.date_util import DateUtil
11
+ from whatap.util.hexa32 import Hexa32 as hexa32
12
+ from whatap.util.keygen import KeyGen
13
+ import whatap.util.throttle_util as throttle_util
14
+ import whatap.net.async_sender as async_sender
15
+ from whatap.net.packet_type_enum import PacketTypeEnum
16
+ from whatap import logging
17
+
18
+
19
+ from functools import wraps
20
+ import logging as logging_module
21
+ import datetime, sys
22
+
23
+ SCOPE_ARGS_LENGTH = 2
24
+ HEADER = 'headers'
25
+ PATH = 'path'
26
+ USER_AGENT = 'user-agent'
27
+ REFERER = 'referer'
28
+ CLIENT = 'client'
29
+ COOKIE = 'cookie'
30
+ HOST = 'host'
31
+ QUERY_STRING = 'query_string'
32
+ WHATAP_CTX = '__whatap__ctx'
33
+
34
+ logger = logging_module.getLogger(__name__)
35
+
36
+ def blocking_handler_async():
37
+ def handler(func):
38
+ from django.http import HttpResponse
39
+ async def wrapper(*args, **kwargs):
40
+ scope, headers = parseHeaders(args)
41
+
42
+ if conf.throttle_enabled and scope and headers:
43
+ remote_ip = parseRemoteAddr(scope, headers)
44
+ path = scope.get(PATH)
45
+ if throttle_util.isblocking(remote_ip, path):
46
+ if conf.reject_event_enabled:
47
+ ctx = TraceContextManager.getLocalContext()
48
+ if not ctx:
49
+ ctx = TraceContext()
50
+
51
+ throttle_util.sendrejectevent(ctx, path, remote_ip)
52
+ if conf.throttle_blocked_forward:
53
+ response = HttpResponse(status=302)
54
+ response['Location'] = conf.throttle_blocked_forward
55
+ return response
56
+ status = '403 Forbidden'
57
+ start_response= args[2]
58
+ start_response(status, [])
59
+ response = HttpResponse(content=conf.throttle_blocked_message, status=403)
60
+ return response
61
+ return await func(*args, **kwargs)
62
+
63
+ return wrapper
64
+
65
+ return handler
66
+
67
+ def trace_handler_async(fn, start=False):
68
+ def handler(func):
69
+ @wraps(func)
70
+ async def wrapper(*args, **kwargs):
71
+ ctx = TraceContextManager.getLocalContext()
72
+ if not start and not ctx:
73
+ return fn(*args, **kwargs)
74
+
75
+ try:
76
+ callback = await func(*args, **kwargs)
77
+ except Exception as e:
78
+ ctx = TraceContextManager.getLocalContext()
79
+ if ctx and ctx.error_step ==e:
80
+ ctx.error_step= None
81
+ raise e
82
+ logging.debug(e, extra={'id': 'WA917'}, exc_info=True)
83
+ print(e, dict(extra={'id': 'WA917'}, exc_info=True))
84
+ import traceback
85
+ traceback.print_exc()
86
+
87
+ return await fn(*args, **kwargs)
88
+ else:
89
+ if ctx and ctx.error_step:
90
+ e = ctx.error_step
91
+ ctx.error_step = None
92
+ raise e
93
+ return callback
94
+
95
+ return wrapper
96
+
97
+ return handler
98
+
99
+ def instrument_handlers_async(module):
100
+ def get_response_wrapper(fn):
101
+ @trace_handler_async(fn, start=True)
102
+ async def trace(*args, **kwargs):
103
+
104
+ print('starting wrapped:', args, kwargs)
105
+ # request = get_request_fromargs(args)
106
+ startedAt = datetime.datetime.Now()
107
+ callback = await fn(*args, **kwargs)
108
+
109
+ print('elapsed:', datetime.datetime.now() - startedAt )
110
+ return callback
111
+ print('wrapper called')
112
+ return trace
113
+
114
+ if hasattr(module, 'BaseHandler') and hasattr(module.BaseHandler, 'get_response_async'):
115
+ module.BaseHandler.get_response_async = get_response_wrapper(module.BaseHandler.get_response_async)
116
+
117
+ def parseHeaders(args):
118
+ headers = {}
119
+ if len(args) > SCOPE_ARGS_LENGTH:
120
+ scope = args[1]
121
+ if HEADER in scope:
122
+ for arg in scope[HEADER]:
123
+ if arg and len(arg) == 2 and arg[0] and arg[1]:
124
+ headers[arg[0].decode('utf8').lower()] = arg[1].decode('utf8')
125
+
126
+ return scope, headers
127
+ return None, None
128
+
129
+ def parseRemoteAddr(scope, headers):
130
+ remoteIp = ''
131
+ if CLIENT in scope:
132
+ remoteIp = scope.get(CLIENT)[0]
133
+ if conf.trace_http_client_ip_header_key:
134
+ header_val = headers.get(conf.trace_http_client_ip_header_key, '')
135
+ remoteIp = header_val.split(',')[0].strip()
136
+
137
+ return remoteIp
138
+
139
+ def getUserId(scope, headers, defValue):
140
+ try:
141
+ if conf.user_header_ticket:
142
+ ticket = headers.get(conf.user_header_ticket, "")
143
+ if ticket:
144
+ return hash_util.hashFromString(ticket), ticket
145
+ return 0, ""
146
+ cookie = headers.get(COOKIE, "")
147
+ if cookie:
148
+ if len(cookie) >= conf.trace_user_cookie_limit :
149
+ return hash_util.hashFromString(defValue) if defValue else 0, defValue or ""
150
+
151
+ x1 = cookie.find(UseridUtil.WHATAP_R)
152
+ if x1 >= 0:
153
+ x2 = cookie.find(';', x1)
154
+ if x2 > 0:
155
+ value = cookie[x1 + len(UseridUtil.WHATAP_R) + 1: x2]
156
+ else:
157
+ value = cookie[x1 + len(UseridUtil.WHATAP_R) + 1:]
158
+ return hexa32.toLong32(value), value
159
+ userid = KeyGen.next()
160
+ return userid, hexa32.toString32(userid)
161
+ except Exception:
162
+ exc_type, exc_value, exc_traceback = sys.exc_info()
163
+ logger.debug("A502", 10, str(exc_value))
164
+ return hash_util.hashFromString(defValue) if defValue else 0, defValue or ""
165
+
166
+ async def interceptor_async(fn, *args, **kwargs):
167
+
168
+ scope, headers = parseHeaders(args)
169
+ if scope == None and headers == None:
170
+ return await fn(*args, **kwargs)
171
+
172
+ ctx = TraceContext()
173
+ ctx.host = headers.get(HOST, '').split(':')[0]
174
+ ctx.service_name = scope.get(PATH)
175
+
176
+ ctx.remoteIp = parseRemoteAddr(scope, headers)
177
+
178
+ ctx.userAgentString = headers.get(USER_AGENT, '')
179
+ ctx.referer = headers.get(REFERER, '')
180
+
181
+ if conf.trace_user_enabled:
182
+ if conf.trace_user_using_ip:
183
+ ctx.userid = parseRemoteAddr(scope, headers)
184
+ else:
185
+ ctx.userid, ctx._rawuserid = getUserId(scope, headers, ctx.remoteIp)
186
+
187
+ mstt = headers.get(
188
+ conf._trace_mtrace_caller_key.lower().replace('-', '_'), '')
189
+
190
+ if mstt:
191
+ ctx.setTransfer(mstt)
192
+ if conf.stat_mtrace_enabled:
193
+ val = headers.get(
194
+ conf._trace_mtrace_info_key.lower().replace('-', '_'), '')
195
+ if val and len(val):
196
+ ctx.setTransferInfo(val)
197
+ pass
198
+
199
+ myid = headers.get(
200
+ conf._trace_mtrace_callee_key.lower().replace('-', '_'), '')
201
+ if myid:
202
+ ctx.setTxid(myid)
203
+ caller_poid = headers.get(
204
+ conf._trace_mtrace_caller_poid_key.upper().replace('-', '_'), '')
205
+
206
+ if caller_poid:
207
+ ctx.mcaller_poid = caller_poid
208
+
209
+ try:
210
+ if isIgnore(ctx.service_name):
211
+ ctx.is_ignored = True
212
+ return fn(*args, **kwargs)
213
+ except Exception as e:
214
+ pass
215
+
216
+ start_interceptor(ctx)
217
+
218
+ try:
219
+ scope[WHATAP_CTX]= ctx
220
+ callback = await fn(*args, **kwargs)
221
+ query_string = str(scope.get(QUERY_STRING, ''))
222
+ if query_string:
223
+ ctx.service_name += '?{}'.format(query_string)
224
+
225
+ if ctx.service_name.find('.') > -1 and ctx.service_name.split('.')[
226
+ 1] in conf.web_static_content_extensions:
227
+ ctx.isStaticContents = 'true'
228
+
229
+ if getattr(callback, 'status_code', None):
230
+ status_code = callback.status_code
231
+ errors = [callback.reason_phrase, callback.__class__.__name__]
232
+ interceptor_error(status_code, errors)
233
+
234
+ if conf.profile_http_header_enabled:
235
+ keys = []
236
+ for key, value in headers.items():
237
+ keys.append(key)
238
+ keys.sort()
239
+
240
+ text = ''
241
+ for key in keys:
242
+ text += '{}={}\n'.format(key.lower(),
243
+ headers[key])
244
+
245
+ datas = ['HTTP-HEADERS', 'HTTP-HEADERS', text]
246
+ ctx.start_time = DateUtil.nowSystem()
247
+ async_sender.send_packet(PacketTypeEnum.TX_MSG, ctx, datas)
248
+ finally:
249
+ if ctx:
250
+ end_interceptor(ctx=ctx)
251
+
@@ -0,0 +1,31 @@
1
+ """FastAPI / Starlette ASGI 계측 패키지.
2
+
3
+ 외부 공개 심볼 — 변경 금지:
4
+ - instrument_applications (trace_module_definition.py)
5
+ - instrument (trace_module_definition.py)
6
+ - instrument_util (trace_module_definition.py)
7
+ - interceptor_error_log (whatap/__init__.py)
8
+
9
+ 모듈 구성:
10
+ scope scope/headers 파싱 + ctx 주입 (parse_headers, client_ip,
11
+ resolve_user_id, populate_ctx)
12
+ transaction TX 라이프사이클 (is_ignored, begin, finish)
13
+ endpoint route template · 쿠키 · SSE 래핑 (upgrade_service_name,
14
+ write_whatap_cookie, wrap_streaming_response)
15
+ exception_log interceptor_error_log
16
+ instrumentation 훅 엔트리 3 개 (instrument_applications / instrument /
17
+ instrument_util)
18
+ """
19
+ from whatap.trace.mod.application.fastapi.exception_log import interceptor_error_log
20
+ from whatap.trace.mod.application.fastapi.instrumentation import (
21
+ instrument,
22
+ instrument_applications,
23
+ instrument_util,
24
+ )
25
+
26
+ __all__ = [
27
+ 'instrument_applications',
28
+ 'instrument',
29
+ 'instrument_util',
30
+ 'interceptor_error_log',
31
+ ]
@@ -0,0 +1,73 @@
1
+ """엔드포인트 도달 시에만 유효한 메타 / 응답 보강.
2
+
3
+ TX 라이프사이클에는 관여하지 않는다 (transaction.py 전담).
4
+ """
5
+ import logging as logging_module
6
+ import sys
7
+
8
+ from whatap.conf.configure import Configure as conf
9
+ from whatap.trace.trace_error import interceptor_step_error
10
+ from whatap.util import bit_util
11
+ from whatap.util.userid_util import UseridUtil
12
+
13
+ logger = logging_module.getLogger(__name__)
14
+
15
+
16
+ def upgrade_service_name(ctx, dependant):
17
+ """raw path(/items/42) → route template(/items/{item_id})."""
18
+ if dependant is None:
19
+ return
20
+ path = getattr(dependant, 'path', None)
21
+ if path:
22
+ ctx.service_name = path
23
+
24
+
25
+ def write_whatap_cookie(environ, response, raw_user_id):
26
+ """응답 객체에 WhaTap userid 쿠키 기록. 조건 미충족 시 no-op."""
27
+ if response is None or not raw_user_id:
28
+ return
29
+ if not conf.trace_user_enabled or conf.trace_user_using_ip:
30
+ return
31
+ if conf.user_header_ticket:
32
+ return
33
+
34
+ cookie = environ.get('cookie') if environ else None
35
+ if (cookie
36
+ and cookie.find(UseridUtil.WHATAP_R) >= 0
37
+ and cookie.find(raw_user_id) >= 0):
38
+ return
39
+
40
+ try:
41
+ response.set_cookie(
42
+ key=UseridUtil.WHATAP_R,
43
+ value=raw_user_id,
44
+ max_age=bit_util.INT_MAX_VALUE,
45
+ path='/',
46
+ domain=conf.trace_user_cookie_domain,
47
+ )
48
+ except Exception:
49
+ _, exc, _ = sys.exc_info()
50
+ logger.debug('A503', 10, str(exc))
51
+
52
+
53
+ def wrap_streaming_response(callback):
54
+ """StreamingResponse body_iterator 에 step-error 포착 래퍼 장착."""
55
+ try:
56
+ from fastapi.responses import StreamingResponse
57
+ except ImportError:
58
+ return callback
59
+ if not isinstance(callback, StreamingResponse):
60
+ return callback
61
+
62
+ original = callback.body_iterator
63
+
64
+ async def _traced():
65
+ try:
66
+ async for chunk in original:
67
+ yield chunk
68
+ except Exception as exc:
69
+ interceptor_step_error(exc)
70
+ raise
71
+
72
+ callback.body_iterator = _traced()
73
+ return callback
@@ -0,0 +1,63 @@
1
+ """엔드포인트 실행 중 발생한 unhandled exception 을 LogSink 로 전송.
2
+
3
+ 외부(whatap/__init__.py)가 `interceptor_error_log` 이름으로 직접 import 하므로
4
+ 함수명 · 시그니처 변경 금지.
5
+ """
6
+ import inspect
7
+ import linecache
8
+ import sys
9
+ import traceback
10
+
11
+ from whatap.conf.configure import Configure as conf
12
+ import whatap.io as whatapio
13
+ import whatap.net.async_sender as async_sender
14
+ import whatap.pack.logSinkPack as logSinkPack
15
+ from whatap.util.date_util import DateUtil
16
+
17
+
18
+ def interceptor_error_log(trxid, e, fn, args, kwargs):
19
+ if conf.log_unhandled_exception == 'false':
20
+ return
21
+
22
+ error_class = ''
23
+ if e.args:
24
+ error_class = str(e.args[0])
25
+ elif hasattr(e, 'detail'):
26
+ error_class = getattr(e, 'detail')
27
+
28
+ _, _, tb = sys.exc_info()
29
+ last_filename = last_lineno = last_globals = None
30
+ for frame, lineno in traceback.walk_tb(tb):
31
+ last_filename = frame.f_code.co_filename
32
+ last_lineno = lineno
33
+ last_globals = frame.f_globals
34
+
35
+ fields = {
36
+ 'errorClass': error_class,
37
+ 'args': str(inspect.getcallargs(fn, *args, **kwargs)),
38
+ }
39
+ content = ''
40
+ if last_filename and last_lineno:
41
+ fields['filename'] = last_filename
42
+ fields['lineno'] = last_lineno
43
+ linecache.checkcache(last_filename)
44
+ for i, line in enumerate(linecache.getlines(last_filename, last_globals)):
45
+ if i == last_lineno - 1 and len(line.lstrip()) > 2:
46
+ indent = len(line) - len(line.lstrip())
47
+ content += ''.join(['-' * (indent - 1) + '>', line.lstrip()])
48
+ else:
49
+ content += line
50
+
51
+ pack = logSinkPack.getLogSinkPack(
52
+ t=DateUtil.now(),
53
+ category='UnhandledException',
54
+ tags={'@txid': trxid},
55
+ fields=fields,
56
+ line=DateUtil.now(),
57
+ content=content,
58
+ )
59
+ pack.pcode = conf.PCODE
60
+
61
+ buf = whatapio.DataOutputX()
62
+ buf.writePack(pack, None)
63
+ async_sender.send_relaypack(buf.toByteArray())
@@ -0,0 +1,204 @@
1
+ """FastAPI 훅 엔트리포인트 — 3 개 모듈 함수.
2
+
3
+ - instrument_applications : fastapi.applications.FastAPI.__call__
4
+ ASGI 진입점. scope 파싱 → ctx 주입 → begin → wrapped_send → finish.
5
+ 미들웨어 early-return / 404 / 405 / CORS preflight 등 엔드포인트
6
+ 미도달 응답도 이 지점에서 캡처된다.
7
+ - instrument : fastapi.routing.run_endpoint_function
8
+ 엔드포인트 도달 시 route template / setUserId 쿠키 / SSE body wrap /
9
+ unhandled exception 로깅. TX 라이프사이클에는 관여하지 않는다.
10
+ - instrument_util : fastapi.dependencies.utils.solve_dependencies
11
+ 엔드포인트에서 쓰기 위한 request / response 를 dependant 에
12
+ `__whatap__` dict 로 부착.
13
+ """
14
+ from copy import copy
15
+
16
+ from whatap.trace.mod.application.fastapi import endpoint, scope, transaction
17
+ from whatap.trace.mod.application.fastapi.exception_log import interceptor_error_log
18
+ from whatap.trace.trace_context import TraceContext, TraceContextManager
19
+ from whatap.trace.trace_error import interceptor_step_error
20
+ from whatap.trace.trace_handler import async_trace_handler, trace_handler
21
+
22
+ _WHATAP_DICT = '__whatap__'
23
+ _DEPENDANT = 'dependant'
24
+ _REQUEST = 'request'
25
+ _RESPONSE = 'response'
26
+ _REMOTE_ADDR = 'remoteAddr'
27
+
28
+
29
+ # ── FastAPI.__call__ : TX 생명주기 전담 ────────────────────────
30
+
31
+ def instrument_applications(module):
32
+ def wrapper(fn):
33
+ async def trace(instance, asgi_scope, receive, send):
34
+ if asgi_scope.get('type') != 'http':
35
+ await fn(instance, asgi_scope, receive, send)
36
+ return
37
+
38
+ headers = scope.parse_headers(asgi_scope)
39
+ ctx = TraceContext()
40
+ scope.populate_ctx(ctx, asgi_scope, headers)
41
+
42
+ if transaction.is_ignored(ctx.service_name):
43
+ ctx.is_ignored = True
44
+ try:
45
+ await fn(instance, asgi_scope, receive, send)
46
+ finally:
47
+ TraceContextManager.end(ctx.id)
48
+ return
49
+
50
+ transaction.begin(ctx)
51
+
52
+ status_holder = [0]
53
+ original_send = send
54
+
55
+ async def wrapped_send(message):
56
+ if message and message.get('type') == 'http.response.start':
57
+ try:
58
+ status_holder[0] = int(message.get('status') or 0)
59
+ except (TypeError, ValueError):
60
+ pass
61
+ await original_send(message)
62
+
63
+ try:
64
+ await fn(instance, asgi_scope, receive, wrapped_send)
65
+ except Exception as exc:
66
+ # 하위(endpoint wrapper)에서 이미 step_error 를 발사했으면
67
+ # ctx.error == 1. TX_ERROR 중복 방지를 위해 여기서는 스킵.
68
+ if not ctx.error:
69
+ interceptor_step_error(exc)
70
+ raise
71
+ finally:
72
+ try:
73
+ transaction.finish(ctx, asgi_scope, headers, status_holder[0])
74
+ finally:
75
+ TraceContextManager.end(ctx.id)
76
+
77
+ return trace
78
+
79
+ module.FastAPI.__call__ = wrapper(module.FastAPI.__call__)
80
+
81
+
82
+ # ── run_endpoint_function : endpoint 메타 보강만 ──────────────
83
+
84
+ def instrument(module):
85
+ def wrapper(fn):
86
+ @trace_handler(fn, True)
87
+ def trace(*args, **kwargs):
88
+ dependant = kwargs[_DEPENDANT] = copy(kwargs[_DEPENDANT])
89
+ if kwargs.get('is_coroutine'):
90
+ dependant.call = _wrap_async_endpoint(dependant.call, dependant)
91
+ else:
92
+ dependant.call = _wrap_sync_endpoint(dependant.call, dependant)
93
+ return fn(*args, **kwargs)
94
+
95
+ return trace
96
+
97
+ if hasattr(module, 'run_endpoint_function'):
98
+ module.run_endpoint_function = wrapper(module.run_endpoint_function)
99
+
100
+
101
+ def _wrap_sync_endpoint(fn, dependant):
102
+ @trace_handler(fn, True)
103
+ def trace(*args, **kwargs):
104
+ if not hasattr(dependant, _WHATAP_DICT):
105
+ return fn(*args, **kwargs)
106
+ whatap_dict = _pop_whatap_dict(dependant)
107
+ ctx = TraceContextManager.getLocalContext()
108
+ try:
109
+ _pre_endpoint(ctx, dependant, whatap_dict)
110
+ callback = fn(*args, **kwargs)
111
+ return _post_endpoint(ctx, whatap_dict, callback)
112
+ except Exception as exc:
113
+ interceptor_step_error(exc)
114
+ raise
115
+
116
+ return trace
117
+
118
+
119
+ def _wrap_async_endpoint(fn, dependant):
120
+ @async_trace_handler(fn, True)
121
+ async def trace(*args, **kwargs):
122
+ if not hasattr(dependant, _WHATAP_DICT):
123
+ return await fn(*args, **kwargs)
124
+ whatap_dict = _pop_whatap_dict(dependant)
125
+ ctx = TraceContextManager.getLocalContext()
126
+ try:
127
+ _pre_endpoint(ctx, dependant, whatap_dict)
128
+ callback = await fn(*args, **kwargs)
129
+ return _post_endpoint(ctx, whatap_dict, callback)
130
+ except Exception as exc:
131
+ interceptor_step_error(exc)
132
+ if ctx is not None:
133
+ interceptor_error_log(ctx.id, exc, fn, args, kwargs)
134
+ raise
135
+
136
+ return trace
137
+
138
+
139
+ def _pop_whatap_dict(dependant):
140
+ whatap_dict = getattr(dependant, _WHATAP_DICT)
141
+ try:
142
+ delattr(dependant, _WHATAP_DICT)
143
+ except Exception:
144
+ pass
145
+ return whatap_dict
146
+
147
+
148
+ def _pre_endpoint(ctx, dependant, whatap_dict):
149
+ if ctx is None:
150
+ return
151
+ endpoint.upgrade_service_name(ctx, dependant)
152
+ if not ctx.remoteIp:
153
+ fb = whatap_dict.get(_REMOTE_ADDR)
154
+ if fb:
155
+ ctx.remoteIp = fb
156
+
157
+
158
+ def _post_endpoint(ctx, whatap_dict, callback):
159
+ if ctx is None:
160
+ return callback
161
+ environ = whatap_dict.get('headers')
162
+ response = whatap_dict.get(_RESPONSE)
163
+ endpoint.write_whatap_cookie(
164
+ environ, response, getattr(ctx, '_rawuserid', ''),
165
+ )
166
+ return endpoint.wrap_streaming_response(callback)
167
+
168
+
169
+ # ── solve_dependencies : whatap_dict 세팅 ─────────────────────
170
+
171
+ def instrument_util(module):
172
+ def wrapper(fn):
173
+ async def trace(*args, **kwargs):
174
+ whatap_dict = None
175
+ if _DEPENDANT in kwargs:
176
+ whatap_dict = _attach_whatap_dict(kwargs)
177
+ ret = await fn(*args, **kwargs)
178
+ if whatap_dict is not None and hasattr(ret, '__len__') and len(ret) >= 4:
179
+ whatap_dict[_RESPONSE] = ret[3]
180
+ return ret
181
+
182
+ return trace
183
+
184
+ if hasattr(module, 'solve_dependencies'):
185
+ module.solve_dependencies = wrapper(module.solve_dependencies)
186
+
187
+
188
+ def _attach_whatap_dict(kwargs):
189
+ dependant = kwargs[_DEPENDANT]
190
+ whatap_dict = {}
191
+ setattr(dependant, _WHATAP_DICT, whatap_dict)
192
+ request = kwargs.get(_REQUEST)
193
+ if request is not None:
194
+ if request.client is not None:
195
+ remote_addr = request.client.host
196
+ else:
197
+ remote_addr = request.headers.get('x-forwarded-for')
198
+ whatap_dict.update({
199
+ _REMOTE_ADDR: remote_addr,
200
+ 'headers': copy(request.headers),
201
+ 'cookies': copy(request.cookies),
202
+ 'query_params': copy(request.query_params),
203
+ })
204
+ return whatap_dict