cavisson-pythonagent 0.0.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.
- cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
- cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
- cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
- cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
- cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
- pythonagent/__init__.py +22 -0
- pythonagent/agent/__init__.py +219 -0
- pythonagent/agent/internal/__init__.py +1 -0
- pythonagent/agent/internal/agent.py +1651 -0
- pythonagent/agent/internal/framesinfo.py +152 -0
- pythonagent/agent/internal/heap_dump.py +39 -0
- pythonagent/agent/internal/intercept_module.py +70 -0
- pythonagent/agent/internal/logs.py +122 -0
- pythonagent/agent/internal/metadata/__init__.py +0 -0
- pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
- pythonagent/agent/internal/proc_compat.py +75 -0
- pythonagent/agent/internal/profile.py +47 -0
- pythonagent/agent/internal/provider.py +83 -0
- pythonagent/agent/internal/thread_dump.py +85 -0
- pythonagent/agent/internal/udp.py +245 -0
- pythonagent/agent/internal/udp_message.py +800 -0
- pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
- pythonagent/agent/probes/Instrumentation/find.py +243 -0
- pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
- pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
- pythonagent/agent/probes/Instrumentation/parser.py +129 -0
- pythonagent/agent/probes/__init__.py +219 -0
- pythonagent/agent/probes/base.py +303 -0
- pythonagent/agent/probes/cache/__init__.py +51 -0
- pythonagent/agent/probes/cache/redis.py +119 -0
- pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
- pythonagent/agent/probes/coroutines/__init__.py +1 -0
- pythonagent/agent/probes/coroutines/asyncio.py +63 -0
- pythonagent/agent/probes/elasticdb/__init__.py +7 -0
- pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
- pythonagent/agent/probes/frameworks/__init__.py +27 -0
- pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
- pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
- pythonagent/agent/probes/frameworks/aisess.py +151 -0
- pythonagent/agent/probes/frameworks/asgi.py +340 -0
- pythonagent/agent/probes/frameworks/bottle.py +27 -0
- pythonagent/agent/probes/frameworks/cherry.py +25 -0
- pythonagent/agent/probes/frameworks/django.py +128 -0
- pythonagent/agent/probes/frameworks/falcon.py +21 -0
- pythonagent/agent/probes/frameworks/fastapi.py +35 -0
- pythonagent/agent/probes/frameworks/flask.py +30 -0
- pythonagent/agent/probes/frameworks/pyramid.py +56 -0
- pythonagent/agent/probes/frameworks/test.py +108 -0
- pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
- pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
- pythonagent/agent/probes/frameworks/wsgi.py +353 -0
- pythonagent/agent/probes/grpc/__init__.py +76 -0
- pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
- pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
- pythonagent/agent/probes/havoc/__init__.py +0 -0
- pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
- pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
- pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
- pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
- pythonagent/agent/probes/http/__init__.py +49 -0
- pythonagent/agent/probes/http/aiohttp_client.py +59 -0
- pythonagent/agent/probes/http/boto.py +12 -0
- pythonagent/agent/probes/http/httplib.py +110 -0
- pythonagent/agent/probes/http/httpx_client.py +116 -0
- pythonagent/agent/probes/http/requests.py +15 -0
- pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
- pythonagent/agent/probes/http/urllib3.py +16 -0
- pythonagent/agent/probes/langchain/__init__.py +21 -0
- pythonagent/agent/probes/langchain/base_tool.py +95 -0
- pythonagent/agent/probes/langchain/langchain_community.py +136 -0
- pythonagent/agent/probes/langchain/langchain_core.py +32 -0
- pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
- pythonagent/agent/probes/logging/__init__.py +106 -0
- pythonagent/agent/probes/message_brokers/__init__.py +4 -0
- pythonagent/agent/probes/message_brokers/pika.py +126 -0
- pythonagent/agent/probes/mongodb/__init__.py +6 -0
- pythonagent/agent/probes/mongodb/pymongo.py +286 -0
- pythonagent/agent/probes/openai/__init__.py +3 -0
- pythonagent/agent/probes/openai/openai.py +797 -0
- pythonagent/agent/probes/span.py +101 -0
- pythonagent/agent/probes/sql/__init__.py +13 -0
- pythonagent/agent/probes/sql/botocores3.py +51 -0
- pythonagent/agent/probes/sql/dbapi.py +285 -0
- pythonagent/agent/probes/sql/dynamodb.py +90 -0
- pythonagent/agent/probes/sql/mysql_connector.py +24 -0
- pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
- pythonagent/agent/probes/sql/mysqldb.py +43 -0
- pythonagent/agent/probes/sql/psycopg2.py +174 -0
- pythonagent/agent/probes/sql/pymysql.py +25 -0
- pythonagent/bootstrap/__init__.py +0 -0
- pythonagent/bootstrap/cav_gunicorn.py +26 -0
- pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
- pythonagent/bootstrap/run.py +47 -0
- pythonagent/bootstrap/sitecustomize.py +287 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
- pythonagent/config.py +279 -0
- pythonagent/find.py +72 -0
- pythonagent/find_mod_cls_name.py +54 -0
- pythonagent/lang.py +131 -0
- pythonagent/lib.py +91 -0
- pythonagent/main/__init__.py +0 -0
- pythonagent/main/pytrace/__init__.py +79 -0
- pythonagent/main/pytrace/commands/__init__.py +0 -0
- pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
- pythonagent/main/pytrace/commands/run.py +401 -0
- pythonagent/main/pytrace/pytrace.py +133 -0
- pythonagent/main/wsgi.py +6 -0
- pythonagent/main.py +27 -0
- pythonagent/run.py +46 -0
- pythonagent/sqins.py +8 -0
- pythonagent/test.py +73 -0
- pythonagent/utils.py +168 -0
- pythonagent/vendor/__init__.py +0 -0
- pythonagent/vendor/pympler/__init__.py +1 -0
- pythonagent/vendor/pympler/asizeof.py +2810 -0
- pythonagent/vendor/pympler/charts.py +62 -0
- pythonagent/vendor/pympler/classtracker.py +590 -0
- pythonagent/vendor/pympler/classtracker_stats.py +780 -0
- pythonagent/vendor/pympler/garbagegraph.py +80 -0
- pythonagent/vendor/pympler/mprofile.py +97 -0
- pythonagent/vendor/pympler/muppy.py +275 -0
- pythonagent/vendor/pympler/panels.py +115 -0
- pythonagent/vendor/pympler/process.py +238 -0
- pythonagent/vendor/pympler/py.typed +0 -0
- pythonagent/vendor/pympler/refbrowser.py +451 -0
- pythonagent/vendor/pympler/refgraph.py +350 -0
- pythonagent/vendor/pympler/summary.py +321 -0
- pythonagent/vendor/pympler/tracker.py +267 -0
- pythonagent/vendor/pympler/util/__init__.py +0 -0
- pythonagent/vendor/pympler/util/bottle.py +3809 -0
- pythonagent/vendor/pympler/util/compat.py +23 -0
- pythonagent/vendor/pympler/util/stringutils.py +77 -0
- pythonagent/vendor/pympler/web.py +346 -0
- pythonagent/vendor/werkzeug/__init__.py +20 -0
- pythonagent/vendor/werkzeug/_compat.py +228 -0
- pythonagent/vendor/werkzeug/_internal.py +473 -0
- pythonagent/vendor/werkzeug/_reloader.py +341 -0
- pythonagent/vendor/werkzeug/datastructures.py +3120 -0
- pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
- pythonagent/vendor/werkzeug/debug/console.py +218 -0
- pythonagent/vendor/werkzeug/debug/repr.py +297 -0
- pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
- pythonagent/vendor/werkzeug/exceptions.py +829 -0
- pythonagent/vendor/werkzeug/filesystem.py +64 -0
- pythonagent/vendor/werkzeug/formparser.py +584 -0
- pythonagent/vendor/werkzeug/http.py +1307 -0
- pythonagent/vendor/werkzeug/local.py +420 -0
- pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
- pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
- pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
- pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
- pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
- pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
- pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
- pythonagent/vendor/werkzeug/posixemulation.py +117 -0
- pythonagent/vendor/werkzeug/routing.py +2210 -0
- pythonagent/vendor/werkzeug/security.py +249 -0
- pythonagent/vendor/werkzeug/serving.py +1117 -0
- pythonagent/vendor/werkzeug/test.py +1123 -0
- pythonagent/vendor/werkzeug/testapp.py +241 -0
- pythonagent/vendor/werkzeug/urls.py +1138 -0
- pythonagent/vendor/werkzeug/useragents.py +202 -0
- pythonagent/vendor/werkzeug/utils.py +778 -0
- pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
- pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
- pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
- pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
- pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
- pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
- pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
- pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
- pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
- pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
- pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
- pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
- pythonagent/vendor/werkzeug/wsgi.py +1000 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
|
|
2
|
+
"""This module contains all interactions with raw Python frame objects.
|
|
3
|
+
|
|
4
|
+
This module's public functions should never ever return a raw frame object to
|
|
5
|
+
the caller. To explain all the weird try: finally: del frame stuff, see:
|
|
6
|
+
https://docs.python.org/3.4/library/inspect.html#the-interpreter-stack.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import unicode_literals
|
|
11
|
+
import sys
|
|
12
|
+
import traceback
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_internal_frame(filename):
|
|
16
|
+
"""Return True if this filename belongs to an internal CavAgent module.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
return 'site-packages/CavAgent' in filename
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FrameInfo(object):
|
|
23
|
+
"""Copy of the important info from a Python frame.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
__slots__ = ('class_name', 'name', 'filename', 'lineno', 'internal')
|
|
27
|
+
|
|
28
|
+
def __init__(self, frame):
|
|
29
|
+
try:
|
|
30
|
+
try:
|
|
31
|
+
self.class_name = frame.f_locals['self'].__class__.__name__
|
|
32
|
+
except:
|
|
33
|
+
self.class_name = None
|
|
34
|
+
|
|
35
|
+
code = frame.f_code
|
|
36
|
+
self.name = code.co_name
|
|
37
|
+
self.filename = code.co_filename
|
|
38
|
+
self.lineno = code.co_firstlineno
|
|
39
|
+
self.internal = is_internal_frame(self.filename)
|
|
40
|
+
finally:
|
|
41
|
+
del frame
|
|
42
|
+
|
|
43
|
+
def __eq__(self, rhs):
|
|
44
|
+
try:
|
|
45
|
+
return (
|
|
46
|
+
self.name == rhs.name and
|
|
47
|
+
self.filename == rhs.filename and
|
|
48
|
+
self.lineno == rhs.lineno and
|
|
49
|
+
self.class_name == rhs.class_name
|
|
50
|
+
)
|
|
51
|
+
except:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def __ne__(self, rhs):
|
|
55
|
+
return not self == rhs
|
|
56
|
+
|
|
57
|
+
def __hash__(self):
|
|
58
|
+
return hash((self.name, self.filename, self.lineno))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _frame_hash(frame):
|
|
62
|
+
try:
|
|
63
|
+
code = frame.f_code
|
|
64
|
+
key = (code.co_name, code.co_filename, code.co_firstlineno)
|
|
65
|
+
return hash(key)
|
|
66
|
+
finally:
|
|
67
|
+
del frame
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_root_frame(thread_id):
|
|
71
|
+
# Internal use only - get the root frame for the specified thread.
|
|
72
|
+
frames = sys._current_frames()
|
|
73
|
+
try:
|
|
74
|
+
return frames.get(thread_id, None)
|
|
75
|
+
finally:
|
|
76
|
+
del frames
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_formatted_stack(thread_id):
|
|
80
|
+
"""Return a list of formatted strings representing the current stack.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
root = _get_root_frame(thread_id)
|
|
84
|
+
try:
|
|
85
|
+
return traceback.format_stack(root)
|
|
86
|
+
finally:
|
|
87
|
+
del root
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_frame_info(level):
|
|
91
|
+
"""Drop in replacement for `sys._getframe` which returns a FrameInfo object.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
frame = sys._getframe(level + 1)
|
|
95
|
+
try:
|
|
96
|
+
return FrameInfo(frame)
|
|
97
|
+
finally:
|
|
98
|
+
del frame
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_stack(frame_cache, from_root=True, thread_id=None, greenlet_ref=None):
|
|
102
|
+
"""Generate a list of FrameInfo objects representing the python call stack.
|
|
103
|
+
|
|
104
|
+
By default this function tries to generate a full call stack from the root
|
|
105
|
+
frame on the specified thread or greenlet id. When calling this function
|
|
106
|
+
for this purpose a thread_id must *always* be specified.
|
|
107
|
+
|
|
108
|
+
Passing `from_root=False` will generate a stack from the current frame
|
|
109
|
+
outwards. When calling in this way a thread or greenlet id is not required.
|
|
110
|
+
|
|
111
|
+
`frame_cache` is a dict which is used to cache FrameInfo objects in order
|
|
112
|
+
to reduce the memory footprint of generating stacks with many duplicate
|
|
113
|
+
frames. The management of this cache is left up to the caller.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
frame = None
|
|
118
|
+
try:
|
|
119
|
+
if from_root:
|
|
120
|
+
if greenlet_ref:
|
|
121
|
+
ctx = greenlet_ref()
|
|
122
|
+
if ctx:
|
|
123
|
+
# note that ctx.gr_frame may be none indicating that the current BT is not running
|
|
124
|
+
# under the context of greenlet but thread
|
|
125
|
+
frame = ctx.gr_frame or _get_root_frame(thread_id)
|
|
126
|
+
else:
|
|
127
|
+
return None
|
|
128
|
+
else:
|
|
129
|
+
frame = _get_root_frame(thread_id)
|
|
130
|
+
|
|
131
|
+
if frame is None:
|
|
132
|
+
return None
|
|
133
|
+
else:
|
|
134
|
+
frame = sys._getframe()
|
|
135
|
+
|
|
136
|
+
stack = []
|
|
137
|
+
while frame:
|
|
138
|
+
f_hash = _frame_hash(frame)
|
|
139
|
+
try:
|
|
140
|
+
stack.append(frame_cache[f_hash])
|
|
141
|
+
except KeyError:
|
|
142
|
+
f_info = FrameInfo(frame)
|
|
143
|
+
frame_cache[f_hash] = f_info
|
|
144
|
+
stack.append(f_info)
|
|
145
|
+
|
|
146
|
+
next_frame = frame.f_back
|
|
147
|
+
del frame
|
|
148
|
+
frame = next_frame
|
|
149
|
+
del next_frame
|
|
150
|
+
return stack
|
|
151
|
+
finally:
|
|
152
|
+
del frame
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
def create_heap_dump_muppy(self):
|
|
3
|
+
try:
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Use bundled Pympler library from agent vendor package to avoid external dependency installation.
|
|
8
|
+
|
|
9
|
+
from pythonagent.vendor.pympler import muppy, summary
|
|
10
|
+
all_objects = muppy.get_objects()
|
|
11
|
+
sum1 = summary.summarize(all_objects)
|
|
12
|
+
sum1_sorted = sorted(sum1, key=lambda x: x[2], reverse=True)
|
|
13
|
+
|
|
14
|
+
all_list = []
|
|
15
|
+
for item in sum1_sorted: # Iterate over the sorted data
|
|
16
|
+
obj_type = item[0] # Object type
|
|
17
|
+
obj_count = item[1] # Object count
|
|
18
|
+
obj_size = item[2] # Size in bytes
|
|
19
|
+
formatted_line = f"{obj_type: <40}\t{obj_count: <12}\t{obj_size: <15}"
|
|
20
|
+
|
|
21
|
+
all_list.append(formatted_line)
|
|
22
|
+
|
|
23
|
+
# Add header with fixed width
|
|
24
|
+
header = f"{'types': <40}\t{'# objects': <12}\t{'total size(bytes)': <15}"
|
|
25
|
+
|
|
26
|
+
all_list.insert(0, header)
|
|
27
|
+
return "\n".join(all_list)
|
|
28
|
+
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
self.logger.error(f"Pympler library is not found{e}")
|
|
31
|
+
return "\nUnable to capture heap dump \n\n\n"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
except Exception as e:
|
|
36
|
+
self.logger.error(f"Exception in heap dump{e}")
|
|
37
|
+
return "\nUnable to capture heap dump\n\n\n"
|
|
38
|
+
|
|
39
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from pythonagent.agent.probes import frameworks, http, sql, logging, mongodb, cache, message_brokers, coroutines, openai, langchain, elasticdb, Instrumentation
|
|
4
|
+
|
|
5
|
+
BT = (
|
|
6
|
+
# # Entry points
|
|
7
|
+
('flask', frameworks.intercept_flask),
|
|
8
|
+
('django.core.handlers.wsgi', frameworks.intercept_django_wsgi_handler),
|
|
9
|
+
('django.core.handlers.base', frameworks.intercept_django_base_handler),
|
|
10
|
+
('django.core.handlers.asgi', frameworks.intercept_django_asgi_handler),
|
|
11
|
+
('bottle', frameworks.intercept_bottle),
|
|
12
|
+
('aiohttp.web_app', frameworks.intercept_aiohttp_web),
|
|
13
|
+
('tornado.web', frameworks.intercept_tornado_async_web),
|
|
14
|
+
('cherrypy', frameworks.intercept_cherrypy),
|
|
15
|
+
('pyramid', frameworks.intercept_pyramid),
|
|
16
|
+
('falcon', frameworks.intercept_falcon),
|
|
17
|
+
('starlette.routing', frameworks.intercept_fastapi_asgi_handler),
|
|
18
|
+
('uvicorn.config', frameworks.intercept_uvicorn_config),
|
|
19
|
+
# # HTTP exit calls
|
|
20
|
+
('httplib', http.intercept_httplib),
|
|
21
|
+
('http.client', http.intercept_httplib),
|
|
22
|
+
('urllib3', http.intercept_urllib3),
|
|
23
|
+
('requests', http.intercept_requests),
|
|
24
|
+
('tornado.httpclient', http.intercept_tornado_httpclient),
|
|
25
|
+
('aiohttp.client', http.intercept_aiohttp_client),
|
|
26
|
+
('httpx', http.intercept_httpx_client),
|
|
27
|
+
#('boto.https_connection', http.intercept_boto),
|
|
28
|
+
|
|
29
|
+
# # Logging
|
|
30
|
+
('logging', logging.intercept_logging),
|
|
31
|
+
|
|
32
|
+
# # SQL exit calls
|
|
33
|
+
('psycopg2', sql.psycopg2.intercept_psycopg2_connection),
|
|
34
|
+
('pymysql.connections', sql.pymysql.intercept_pymysql_connections),
|
|
35
|
+
('mysql.connector.connection', sql.mysql_connector.intercept_mysql_connector_connection),
|
|
36
|
+
('mysql.connector.connection_cext', sql.mysql_connector_cext.intercept_mysql_connector_cext_connection),
|
|
37
|
+
('MySQLdb.connections', sql.mysqldb.intercept_MySQLdb_connection),
|
|
38
|
+
# # MongoDB
|
|
39
|
+
('pymongo', mongodb.intercept_pymongo),
|
|
40
|
+
#Dynamo
|
|
41
|
+
('botocore.client', sql.dynamodb.intercept_dynamodb),
|
|
42
|
+
('botocore.endpoint', sql.botocores3.intercept_s3),
|
|
43
|
+
|
|
44
|
+
# #REDIS
|
|
45
|
+
('redis.connection', cache.intercept_redis),
|
|
46
|
+
('redis.asyncio.connection', cache.intercept_redis_asyncio),
|
|
47
|
+
('elasticsearch',elasticdb.intercept_elastic),
|
|
48
|
+
|
|
49
|
+
# RABBIT MQ
|
|
50
|
+
('pika', message_brokers.intercept_pika),
|
|
51
|
+
|
|
52
|
+
# Asyncio
|
|
53
|
+
('asyncio', coroutines.intercept_asyncio),
|
|
54
|
+
|
|
55
|
+
# OpenAI
|
|
56
|
+
('openai', openai.intercept_openai),
|
|
57
|
+
|
|
58
|
+
# LangChain
|
|
59
|
+
#('langchain_core.language_models', langchain.intercept_langchain_core_language_models),
|
|
60
|
+
#('langchain_core.runnables', langchain.intercept_langchain_core_runnables),
|
|
61
|
+
('langchain_core.tools', langchain.intercept_langchain_core_tools),
|
|
62
|
+
('langchain_community.vectorstores', langchain.intercept_langchain_community_vectorstores),
|
|
63
|
+
('langchain_community.embeddings', langchain.intercept_langchain_community_embeddings),
|
|
64
|
+
('langchain_openai', langchain.intercept_langchain_openai),
|
|
65
|
+
|
|
66
|
+
# LangGraph
|
|
67
|
+
#('langgraph.pregel', langchain.intercept_langgraph_pregel),
|
|
68
|
+
#('langgraph.utils.runnable', langchain.intercept_langgraph_utils_runnable),
|
|
69
|
+
|
|
70
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import unicode_literals
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
import logging.handlers
|
|
6
|
+
import os.path
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import sys
|
|
11
|
+
from pythonagent import config
|
|
12
|
+
from pythonagent.lib import default_log_formatter, mkdir
|
|
13
|
+
|
|
14
|
+
LOGGING_MAX_BYTES = 10 * 1024 * 1024
|
|
15
|
+
LOGGING_MAX_NUM_FILES = 5
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_log_level():
|
|
19
|
+
default_logging_level = logging.WARNING
|
|
20
|
+
allowed_logging_levels = {'DEBUG': logging.DEBUG,
|
|
21
|
+
'INFO': logging.INFO,
|
|
22
|
+
'WARNING': logging.WARNING,
|
|
23
|
+
'ERROR': logging.ERROR, # Added for testing
|
|
24
|
+
'CRITICAL': logging.CRITICAL # Added for testing
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
level = config.LOGGING_LEVEL.upper()
|
|
28
|
+
ret_val = allowed_logging_levels.get(level, default_logging_level)
|
|
29
|
+
# NOTSET=0
|
|
30
|
+
# DEBUG=10
|
|
31
|
+
# INFO=20
|
|
32
|
+
# WARN=30
|
|
33
|
+
# ERROR=40
|
|
34
|
+
# CRITICAL=50
|
|
35
|
+
return allowed_logging_levels.get(level, default_logging_level)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_log_filename():
|
|
39
|
+
non_alphanumeric = re.compile(r'\W+')
|
|
40
|
+
sanitize = lambda x: non_alphanumeric.sub('_', x)
|
|
41
|
+
filename = 'nd_trace_'+'_'.join(
|
|
42
|
+
map(sanitize, [config.APP_NAME.lower(), config.TIER.lower(), datetime.now().strftime("%Y%m%d%H%M%S"), str(os.getpid())])) + '.log_0'
|
|
43
|
+
return os.path.join(config.LOGS_DIR, filename)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def debug_enabled(logger):
|
|
47
|
+
for handler in logger.handlers:
|
|
48
|
+
if handler.__class__ is logging.StreamHandler:
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def enable_debug(logger):
|
|
54
|
+
logger.setLevel(logging.DEBUG)
|
|
55
|
+
|
|
56
|
+
for handler in logger.handlers:
|
|
57
|
+
handler.setLevel(logging.DEBUG)
|
|
58
|
+
|
|
59
|
+
stream_handler = logging.StreamHandler()
|
|
60
|
+
stream_handler.setLevel(logging.DEBUG)
|
|
61
|
+
stream_handler.setFormatter(default_log_formatter)
|
|
62
|
+
logger.addHandler(stream_handler)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def disable_debug(logger):
|
|
66
|
+
level = get_log_level()
|
|
67
|
+
logger.setLevel(level)
|
|
68
|
+
|
|
69
|
+
handlers = copy.copy(logger.handlers)
|
|
70
|
+
logger.handlers = []
|
|
71
|
+
for handler in handlers:
|
|
72
|
+
if handler.__class__ is not logging.StreamHandler:
|
|
73
|
+
handler.setLevel(level)
|
|
74
|
+
logger.addHandler(handler)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def configure_logging():
|
|
78
|
+
try:
|
|
79
|
+
logger = logging.getLogger('pythonagent.agent')
|
|
80
|
+
level = get_log_level()
|
|
81
|
+
print("Logging level:", level)
|
|
82
|
+
logger.setLevel(level)
|
|
83
|
+
path = get_log_filename()
|
|
84
|
+
|
|
85
|
+
print("Logging file path name", path)
|
|
86
|
+
|
|
87
|
+
agent_env = None
|
|
88
|
+
|
|
89
|
+
if "CAV_APP_AGENT_ENV" in os.environ:
|
|
90
|
+
if os.environ['CAV_APP_AGENT_ENV'] == "AWS_LAMBDA":
|
|
91
|
+
agent_env = "AWS_LAMBDA"
|
|
92
|
+
else:
|
|
93
|
+
agent_env = "NATIVE"
|
|
94
|
+
else:
|
|
95
|
+
agent_env = "NATIVE"
|
|
96
|
+
|
|
97
|
+
if agent_env == "AWS_LAMBDA":
|
|
98
|
+
consolHandler = logging.StreamHandler(sys.stdout)
|
|
99
|
+
consolHandler.setLevel(level)
|
|
100
|
+
consolHandler.setFormatter(default_log_formatter)
|
|
101
|
+
logger.addHandler(consolHandler)
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
mkdir(os.path.dirname(path))
|
|
105
|
+
rotating_file_handler = logging.handlers.RotatingFileHandler(path, maxBytes=LOGGING_MAX_BYTES,
|
|
106
|
+
backupCount=LOGGING_MAX_NUM_FILES - 1)
|
|
107
|
+
# logging.getLogger('pythonagent.agent').addHandler()
|
|
108
|
+
rotating_file_handler.setLevel(level)
|
|
109
|
+
rotating_file_handler.setFormatter(default_log_formatter)
|
|
110
|
+
logger.addHandler(rotating_file_handler)
|
|
111
|
+
|
|
112
|
+
if config.DEBUG_LOG:
|
|
113
|
+
enable_debug(logger)
|
|
114
|
+
|
|
115
|
+
has_handlers = logger.hasHandlers()
|
|
116
|
+
logger.warning("has handlers {}".format(str(has_handlers)))
|
|
117
|
+
logger.propagate = False
|
|
118
|
+
logger.warning("has handlers {}".format(str(has_handlers)))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
except:
|
|
122
|
+
logging.getLogger('pythonagent.agent').exception('Logging configuration failed.')
|
|
File without changes
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Purpose: collect a rich, point-in-time snapshot of the monitored Python application/process/
|
|
2
|
+
# deployment environment, for the auto-discovery response's "****Start Metadata****" block
|
|
3
|
+
# (sent from Agent.autodiscovery_py() in agent/internal/agent/agent.py). Mirrors the sibling
|
|
4
|
+
# NodeJS agent's lib/metaData/agentMetaData.js + lib/autoDiscovery.js buildMetadataBlock(), adapted
|
|
5
|
+
# to Python's runtime (no package.json/npm, no cluster module - platform/os/proc_compat stand in).
|
|
6
|
+
#
|
|
7
|
+
# Deliberately stdlib + /proc only (no psutil or other external dependency) - same constraint as
|
|
8
|
+
# the sibling proc_compat.py this module builds on, and it keeps a metadata-collection failure
|
|
9
|
+
# (e.g. a missing optional dependency) from ever being able to break auto-discovery.
|
|
10
|
+
#
|
|
11
|
+
# Every value collected here is synchronous and local to this process - no shelling out, no
|
|
12
|
+
# network calls - so calling collect_metadata()/build_metadata_block() cannot add meaningful
|
|
13
|
+
# latency to a discovery response. collect_metadata() returns a plain dict; callers are
|
|
14
|
+
# responsible for escaping (escape_metadata_value) and formatting into "key=value;" lines -
|
|
15
|
+
# build_metadata_block() does that end-to-end for the auto-discovery caller.
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import platform
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
from pythonagent.agent.internal.proc_compat import get_process_name, get_virtual_memory, get_rss_bytes
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger('pythonagent.agent')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_VERSION_LINE_PATTERN = re.compile(r'^\s*VERSION\s+(\S+)', re.IGNORECASE | re.MULTILINE)
|
|
29
|
+
_BUILD_LINE_PATTERN = re.compile(r'^\s*BUILD\s+(\S+)', re.IGNORECASE | re.MULTILINE)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_agent_version():
|
|
33
|
+
"""Resolve this agent's actual shipped version/build from the "VERSION x.y.z / BUILD n" file
|
|
34
|
+
that install.sh lays down at $NDHOME/python/version - the same convention the sibling
|
|
35
|
+
.NET/native agents use for their own top-level 'version' file (see e.g.
|
|
36
|
+
cavisson_python_agent/version, dotnetagent/agent/version). This is the authoritative,
|
|
37
|
+
release-tracked version; the 'pythonagent' pip distribution's own version (setup.py) is just a
|
|
38
|
+
placeholder ("0.0.1", "A small example package") and not meaningful here.
|
|
39
|
+
|
|
40
|
+
Returns (version, build) - either may be None (field omitted) if NDHOME isn't set, the file is
|
|
41
|
+
missing, or a line can't be parsed - never raises, since this must not be able to block agent
|
|
42
|
+
metadata collection.
|
|
43
|
+
"""
|
|
44
|
+
ndhome = os.environ.get('NDHOME')
|
|
45
|
+
if not ndhome:
|
|
46
|
+
return None, None
|
|
47
|
+
try:
|
|
48
|
+
with open(os.path.join(ndhome, 'python', 'version'), 'r') as f:
|
|
49
|
+
contents = f.read()
|
|
50
|
+
except (IOError, OSError):
|
|
51
|
+
return None, None
|
|
52
|
+
|
|
53
|
+
version_match = _VERSION_LINE_PATTERN.search(contents)
|
|
54
|
+
build_match = _BUILD_LINE_PATTERN.search(contents)
|
|
55
|
+
return (
|
|
56
|
+
version_match.group(1) if version_match else None,
|
|
57
|
+
build_match.group(1) if build_match else None,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Resolved once at import time - the installed version file can't change within a process lifetime.
|
|
62
|
+
_AGENT_VERSION, _AGENT_BUILD = _resolve_agent_version()
|
|
63
|
+
|
|
64
|
+
# Env var keys whose VALUE gets redacted in the metadata block - matched case-insensitively
|
|
65
|
+
# against the key name, not the value, since we can't know a value is secret just by looking at
|
|
66
|
+
# it. This list intentionally errs toward over-redacting common secret-naming conventions.
|
|
67
|
+
_SECRET_KEY_PATTERN = re.compile(r'key|secret|token|password|pwd|credential|auth|apikey|connectionstring|private', re.IGNORECASE)
|
|
68
|
+
_REDACTED_VALUE = '***REDACTED***'
|
|
69
|
+
|
|
70
|
+
# Env var keys dropped entirely (not just redacted) - not secrets, just noisy shell/terminal
|
|
71
|
+
# config that adds no diagnostic value and is heavy on ";"/"=".
|
|
72
|
+
_EXCLUDED_ENV_KEYS = re.compile(r'^(LS_COLORS|LESSOPEN|LESSCLOSE|XMODIFIERS|DBUS_SESSION_BUS_ADDRESS)$', re.IGNORECASE)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _add_if_defined(target, key, value):
|
|
76
|
+
if value is None or value == '':
|
|
77
|
+
return
|
|
78
|
+
target[key] = value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_process_uptime_sec():
|
|
82
|
+
"""Return seconds since this process started, via /proc only (Linux). Best-effort - returns
|
|
83
|
+
None on any platform/parse failure rather than raising, since it's cosmetic metadata."""
|
|
84
|
+
try:
|
|
85
|
+
clk_tck = os.sysconf('SC_CLK_TCK')
|
|
86
|
+
with open('/proc/uptime', 'r') as f:
|
|
87
|
+
system_uptime = float(f.read().split()[0])
|
|
88
|
+
with open('/proc/self/stat', 'r') as f:
|
|
89
|
+
# Field 22 (starttime, in clock ticks since boot) - skip past the "(comm)" field
|
|
90
|
+
# since it may itself contain spaces/parens.
|
|
91
|
+
stat = f.read()
|
|
92
|
+
fields = stat[stat.rfind(')') + 2:].split()
|
|
93
|
+
starttime_ticks = float(fields[19])
|
|
94
|
+
process_start_uptime = starttime_ticks / clk_tck
|
|
95
|
+
return int(system_uptime - process_start_uptime)
|
|
96
|
+
except (IOError, OSError, ValueError, IndexError, AttributeError):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _collect_agent_identity(meta, agent):
|
|
101
|
+
_add_if_defined(meta, 'agentType', 'Python')
|
|
102
|
+
_add_if_defined(meta, 'agentVersion', _AGENT_VERSION)
|
|
103
|
+
_add_if_defined(meta, 'agentBuild', _AGENT_BUILD)
|
|
104
|
+
_add_if_defined(meta, 'agentTier', getattr(agent, 'tier_name', None))
|
|
105
|
+
_add_if_defined(meta, 'agentServer', getattr(agent, 'server_name', None))
|
|
106
|
+
_add_if_defined(meta, 'agentInstance', getattr(agent, 'instance_name', None))
|
|
107
|
+
_add_if_defined(meta, 'agentEnv', getattr(agent, 'cav_env', None))
|
|
108
|
+
_add_if_defined(meta, 'ndHome', os.environ.get('NDHOME'))
|
|
109
|
+
_add_if_defined(meta, 'testRunning', getattr(agent, 'test_running', None))
|
|
110
|
+
_add_if_defined(meta, 'nfEnabled', getattr(agent, 'nf_enabled', None))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _collect_app_identity(meta):
|
|
114
|
+
try:
|
|
115
|
+
from pythonagent import config
|
|
116
|
+
_add_if_defined(meta, 'appName', getattr(config, 'APP_NAME', None))
|
|
117
|
+
_add_if_defined(meta, 'wsgiModule', getattr(config, 'WSGI_MODULE', None))
|
|
118
|
+
_add_if_defined(meta, 'wsgiCallable', getattr(config, 'WSGI_CALLABLE_OBJECT', None))
|
|
119
|
+
_add_if_defined(meta, 'asgiModule', getattr(config, 'ASGI_MODULE', None))
|
|
120
|
+
_add_if_defined(meta, 'asgiCallable', getattr(config, 'ASGI_CALLABLE_OBJECT', None))
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning('agent_meta_data | Error collecting app identity from config : %s', e)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
_add_if_defined(meta, 'appEntryPoint', get_process_name())
|
|
126
|
+
_add_if_defined(meta, 'appCwd', os.getcwd())
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning('agent_meta_data | Error collecting app identity : %s', e)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _collect_runtime_versions(meta):
|
|
132
|
+
try:
|
|
133
|
+
_add_if_defined(meta, 'pythonVersion', platform.python_version())
|
|
134
|
+
_add_if_defined(meta, 'pythonImplementation', platform.python_implementation())
|
|
135
|
+
_add_if_defined(meta, 'pythonCompiler', platform.python_compiler())
|
|
136
|
+
_add_if_defined(meta, 'pythonBuild', ' '.join(platform.python_build()))
|
|
137
|
+
_add_if_defined(meta, 'isVirtualenv', hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning('agent_meta_data | Error collecting runtime versions : %s', e)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _collect_os_info(meta):
|
|
143
|
+
try:
|
|
144
|
+
_add_if_defined(meta, 'osType', platform.system())
|
|
145
|
+
_add_if_defined(meta, 'osPlatform', sys.platform)
|
|
146
|
+
_add_if_defined(meta, 'osRelease', platform.release())
|
|
147
|
+
_add_if_defined(meta, 'osArch', platform.machine())
|
|
148
|
+
_add_if_defined(meta, 'hostname', platform.node())
|
|
149
|
+
_add_if_defined(meta, 'cpuCount', os.cpu_count() if hasattr(os, 'cpu_count') else None)
|
|
150
|
+
|
|
151
|
+
total, available, _percent, _used, _free = get_virtual_memory()
|
|
152
|
+
_add_if_defined(meta, 'totalMemBytes', total or None)
|
|
153
|
+
_add_if_defined(meta, 'freeMemBytesAtDiscovery', available or None)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with open('/proc/uptime', 'r') as f:
|
|
157
|
+
_add_if_defined(meta, 'osUptimeSec', int(float(f.read().split()[0])))
|
|
158
|
+
except (IOError, OSError, ValueError, IndexError):
|
|
159
|
+
pass
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.warning('agent_meta_data | Error collecting OS info : %s', e)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _collect_process_snapshot(meta):
|
|
165
|
+
try:
|
|
166
|
+
_add_if_defined(meta, 'pid', os.getpid())
|
|
167
|
+
_add_if_defined(meta, 'ppid', os.getppid() if hasattr(os, 'getppid') else None)
|
|
168
|
+
_add_if_defined(meta, 'execPath', sys.executable)
|
|
169
|
+
# Deliberately interpreter flags/warnoptions (sys.flags/sys.warnoptions), NOT sys.argv[1:] -
|
|
170
|
+
# the application's own command-line arguments may include secrets, mirroring the NodeJS
|
|
171
|
+
# agent's choice of process.execArgv over process.argv.
|
|
172
|
+
_add_if_defined(meta, 'pythonWarnOptions', ' '.join(sys.warnoptions) if sys.warnoptions else None)
|
|
173
|
+
_add_if_defined(meta, 'processUptimeSec', _get_process_uptime_sec())
|
|
174
|
+
_add_if_defined(meta, 'memRssBytes', get_rss_bytes() or None)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.warning('agent_meta_data | Error collecting process snapshot : %s', e)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _collect_deployment_hints(meta):
|
|
180
|
+
try:
|
|
181
|
+
env = os.environ
|
|
182
|
+
is_docker = False
|
|
183
|
+
try:
|
|
184
|
+
is_docker = os.path.exists('/.dockerenv')
|
|
185
|
+
except OSError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
_add_if_defined(meta, 'isDocker', is_docker)
|
|
189
|
+
_add_if_defined(meta, 'isKubernetes', bool(env.get('KUBERNETES_SERVICE_HOST')))
|
|
190
|
+
_add_if_defined(meta, 'isAwsLambda', bool(env.get('AWS_LAMBDA_FUNCTION_NAME') or env.get('AWS_EXECUTION_ENV')))
|
|
191
|
+
_add_if_defined(meta, 'isEcs', bool(env.get('ECS_CONTAINER_METADATA_URI')))
|
|
192
|
+
_add_if_defined(meta, 'isAzureAppService', bool(env.get('WEBSITE_INSTANCE_ID')))
|
|
193
|
+
_add_if_defined(meta, 'isCloudRun', bool(env.get('K_SERVICE')))
|
|
194
|
+
_add_if_defined(meta, 'isHeroku', bool(env.get('DYNO')))
|
|
195
|
+
_add_if_defined(meta, 'isGunicorn', bool(env.get('SERVER_SOFTWARE', '').lower().startswith('gunicorn')))
|
|
196
|
+
_add_if_defined(meta, 'isUwsgi', bool(env.get('UWSGI_ORIGINAL_PROC_NAME')) or 'uwsgi' in sys.modules)
|
|
197
|
+
|
|
198
|
+
# Specific identifying values (operationally useful, not secret-shaped) - only meaningful
|
|
199
|
+
# when the corresponding hint above is true.
|
|
200
|
+
_add_if_defined(meta, 'k8sServiceHost', env.get('KUBERNETES_SERVICE_HOST'))
|
|
201
|
+
_add_if_defined(meta, 'lambdaFunctionName', env.get('AWS_LAMBDA_FUNCTION_NAME'))
|
|
202
|
+
_add_if_defined(meta, 'dyno', env.get('DYNO'))
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.warning('agent_meta_data | Error collecting deployment hints : %s', e)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _collect_launch_context(meta):
|
|
208
|
+
try:
|
|
209
|
+
env = os.environ
|
|
210
|
+
_add_if_defined(meta, 'virtualEnv', env.get('VIRTUAL_ENV'))
|
|
211
|
+
_add_if_defined(meta, 'condaEnv', env.get('CONDA_DEFAULT_ENV'))
|
|
212
|
+
_add_if_defined(meta, 'pythonPath', env.get('PYTHONPATH'))
|
|
213
|
+
|
|
214
|
+
# Multi-worker context (gunicorn/uwsgi pre-fork workers have no direct equivalent of
|
|
215
|
+
# NodeJS's cluster.worker.id, so surface whatever the WSGI server exposes via env).
|
|
216
|
+
_add_if_defined(meta, 'workerId', env.get('APP_WORKER_ID') or env.get('UWSGI_WORKER_ID'))
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.warning('agent_meta_data | Error collecting launch context : %s', e)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _collect_environment_variables(meta):
|
|
222
|
+
try:
|
|
223
|
+
for key, value in os.environ.items():
|
|
224
|
+
if _EXCLUDED_ENV_KEYS.match(key):
|
|
225
|
+
continue
|
|
226
|
+
meta['env.' + key] = _REDACTED_VALUE if _SECRET_KEY_PATTERN.search(key) else value
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.warning('agent_meta_data | Error collecting environment variables : %s', e)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def collect_metadata(agent):
|
|
232
|
+
"""Return a plain dict of metadata key -> raw (unescaped) value for the given Agent instance.
|
|
233
|
+
|
|
234
|
+
None/''-valued entries are never added, so callers can iterate the dict directly without
|
|
235
|
+
extra filtering.
|
|
236
|
+
"""
|
|
237
|
+
meta = {}
|
|
238
|
+
_collect_agent_identity(meta, agent)
|
|
239
|
+
_collect_app_identity(meta)
|
|
240
|
+
_collect_runtime_versions(meta)
|
|
241
|
+
_collect_os_info(meta)
|
|
242
|
+
_collect_process_snapshot(meta)
|
|
243
|
+
_collect_deployment_hints(meta)
|
|
244
|
+
_collect_launch_context(meta)
|
|
245
|
+
_collect_environment_variables(meta)
|
|
246
|
+
return meta
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def escape_metadata_value(value):
|
|
250
|
+
"""Escape a value for embedding in a "key=value;" metadata line.
|
|
251
|
+
|
|
252
|
+
Mirrors the NodeJS agent's AgentSetting.escapeMetadataValue - a value containing ';', '=' or
|
|
253
|
+
a newline (env vars are the likely source) must not be able to corrupt the surrounding
|
|
254
|
+
"key=value;"-per-line, single-block message format.
|
|
255
|
+
"""
|
|
256
|
+
text = value if isinstance(value, str) else str(value)
|
|
257
|
+
return text.replace('\\', '\\\\').replace(';', '\\;').replace('=', '\\=').replace('\n', '\\n').replace('\r', '\\r')
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def build_metadata_block(agent):
|
|
261
|
+
"""Build the "****Start Metadata****...****End Metadata****" block for the auto-discovery
|
|
262
|
+
response, mirroring the NodeJS agent's autoDiscovery.js buildMetadataBlock().
|
|
263
|
+
|
|
264
|
+
Always returns a well-formed (possibly empty) block rather than raising, so a metadata
|
|
265
|
+
collection failure never breaks the rest of the auto-discovery response.
|
|
266
|
+
"""
|
|
267
|
+
lines = []
|
|
268
|
+
try:
|
|
269
|
+
meta = collect_metadata(agent)
|
|
270
|
+
for key, value in meta.items():
|
|
271
|
+
if value is None:
|
|
272
|
+
continue
|
|
273
|
+
lines.append(key + '=' + escape_metadata_value(value) + ';')
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error('agent_meta_data | Error collecting auto-discovery metadata : %s', e)
|
|
276
|
+
return '****Start Metadata****\n' + ''.join(lines) + '\n****End Metadata****\n'
|