Setting the file. One moment.
Di Instrumentation · AWS Observability · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page 69
Creating Amazon Aurora Db Cluster With Instances
92
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Debugging Lambda Timeouts
scripts/cloudwatch/ di_instrumentation.py
Python · 275 lines · 13 KB
15
ARCHITECTURE
16 - The 8 operation implementations (create/list/get/delete/batch-delete-by-scope/
17 batch-delete-by-arns/get-status/check-status) live in the flat `di_*.py` sibling
18 modules. They carry the validation, location/capture parsing, and token-efficient
19 rendering the agent relies on — this is the "ergonomic surface", not a thin boto3
20 passthrough.
21 - The application-signals client seam lives in the leaf module `di_app_signals_client`
22 (`get_application_signals_client()`), which `di_gateway` imports directly. Keeping it out
23 of this entry script is what breaks the old `di_crud_tools/di_status_tools -> di_gateway ->
24 di_instrumentation` import cycle. The operation modules are still imported LAZILY (inside
25 `_dispatch_table`) for a separate reason: they import `botocore` at module top, so a lazy
26 import keeps a bare `import di_instrumentation` free of a hard boto3 dependency (the build
27 env omits boto3 and must still be able to import this module). `--print-contract` itself is
28 NOT boto3-free: it resolves the op functions to inspect their signatures, which triggers
29 the lazy import of the op modules — and hence `botocore` — via `_resolve_tool`.
30
31 LOAD-BEARING DETAILS (keep exactly)
32 - Region resolves from --region > AWS_REGION > AWS_DEFAULT_REGION > us-east-1, at CALL
33 TIME, and the profile region is deliberately ignored. AWS_PROFILE is honored for
34 CREDENTIALS only.
35
36 SECURITY
37 - Credentials are inherited from the ambient boto3 chain and are never logged, echoed, or
38 written. Pass operation arguments as a JSON object via `--json-file PATH` or `--json -`
39 (stdin) so caller-supplied values never transit the shell command line; `--json '<text>'`
40 is also accepted for short, trusted payloads.
41 - Prefer IAM roles (instance profile, ECS task role, or SSO/STS session credentials) over
42 long-lived IAM user access keys — these operations modify live services.
43
44 USAGE
45 python3 scripts/cloudwatch/di_instrumentation.py --print-contract
46 python3 scripts/cloudwatch/di_instrumentation.py <op> --json-file args.json
47 python3 scripts/cloudwatch/di_instrumentation.py <op> --json - # read the JSON object from stdin
48 python3 scripts/cloudwatch/di_instrumentation.py <op> --json '{"...": ...}' # inline (trusted only)
49 """
50
51 from __future__ import annotations
52
53 import argparse
54 import json
55 import os
56 import sys
57 from pathlib import Path
58 from typing import Any, Dict
59
60 # scripts/ is auto-added to sys.path[0] when this file is run directly, so the flat
61 # `di_*.py` siblings import by bare name. Add it explicitly too, so the module also works
62 # when imported (e.g. by a test) rather than executed.
63 _HERE = Path( __file__ ).resolve().parent
64 if str ( _HERE ) not in sys.path:
65 sys.path.insert( 0 , str ( _HERE ))
66
67 # The application-signals client seam now lives in the leaf module di_app_signals_client (see its
68 # WHY THIS EXISTS note); di_gateway imports it directly from there. Moving the seam out of this
69 # entry script is what breaks the old di_gateway -> di_instrumentation cycle. We import only the
70 # API-version constant the contract reports; the module is boto3-free to import, so this does not
71 # pull botocore into a bare `import di_instrumentation`.
72 from di_app_signals_client import APPLICATION_SIGNALS_API_VERSION # noqa: E402
73
74 # ── the 8-op contract: op name -> (vendored module, function) ────────────────────────────
75 # Op names mirror the agent-facing TOOL names (crud_tools/status_tools), not the boto3
76 # method names. Re-verified against registration.py.
77 _OPS = {
78 "create" : ( "di_crud_tools" , "create_instrumentation" ),
79 "list" : ( "di_crud_tools" , "list_instrumentations" ),
80 "get" : ( "di_crud_tools" , "get_instrumentation" ),
81 "delete" : ( "di_crud_tools" , "delete_instrumentation" ),
82 "batch-delete-by-scope" : ( "di_crud_tools" , "batch_delete_instrumentations_by_scope" ),
83 "batch-delete-by-arns" : ( "di_crud_tools" , "batch_delete_instrumentations_by_arns" ),
84 "get-status" : ( "di_status_tools" , "get_instrumentation_configuration_status" ),
85 "check-status" : ( "di_status_tools" , "check_instrumentation_status" ),
86 }
87
88
89 def _dispatch_table () -> Dict[ str , Any]:
90 """Build the op -> function dispatch table by binding each function reference directly.
91
92 NO dynamic dispatch: every function is named as a literal attribute on its freshly
93 imported module (``di_crud_tools.create_instrumentation``), never resolved from a string
94 via ``getattr``/``__import__``. The imports stay inside the function because
95 ``di_crud_tools``/``di_status_tools`` import ``botocore`` at module top; keeping their
96 import lazy here lets a bare ``import di_instrumentation`` stay free of a hard boto3
97 dependency (the build env omits boto3 and must still import this module). Note this does
98 NOT make ``--print-contract`` boto3-free: calling ``_resolve_tool`` runs this function and
99 triggers the lazy ``botocore`` import. (The old import cycle that also required this is
100 gone — the client seam moved to ``di_app_signals_client``.)
101
102 ``_resolve_tool`` and the ``test_dispatch_table_keys_match_ops`` sync guard both key off
103 this table, so an op added to ``_OPS`` without a matching binding here fails loudly rather
104 than silently dropping from the contract.
105 """
106 import di_crud_tools
107 import di_status_tools
108
109 return {
110 "create" : di_crud_tools.create_instrumentation,
111 "list" : di_crud_tools.list_instrumentations,
112 "get" : di_crud_tools.get_instrumentation,
113 "delete" : di_crud_tools.delete_instrumentation,
114 "batch-delete-by-scope" : di_crud_tools.batch_delete_instrumentations_by_scope,
115 "batch-delete-by-arns" : di_crud_tools.batch_delete_instrumentations_by_arns,
116 "get-status" : di_status_tools.get_instrumentation_configuration_status,
117 "check-status" : di_status_tools.check_instrumentation_status,
118 }
119
120
121 def _resolve_tool (op: str ):
122 """Return the vendored tool function for ``op`` from the explicit dispatch table.
123
124 Raises ``KeyError(op)`` for an unknown op (the table is the source of truth for which
125 ops are callable; it is kept in sync with ``_OPS`` by the dispatch sync-guard test).
126 """
127 return _dispatch_table()[op]
128
129
130 # Semantic hints layered onto the inspected signature in the emitted contract. The signature
131 # gives the arg SHAPE (name/required/default); these add the meaning the agent cannot infer
132 # from a bare name — notably that `instrumentation_type` is required on EVERY op (not just
133 # create) and must match how the breakpoint was created.
134 _ARG_HINTS = {
135 "instrumentation_type" : {
136 "enum" : [ "BREAKPOINT" , "PROBE" ],
137 "note" : "required on every op; must match how the breakpoint was created" ,
138 },
139 "service" : {
140 "note" : "service identifier; di_snapshots.py uses the same key `service`" ,
141 },
142 }
143
144
145 def _print_contract () -> int :
146 """Emit the canonical op + arg schema (argument shapes only). SKILL.md and
147 references/ carry the per-operation semantics; this is the argument shape, not the rules.
148 Derived from the operation signatures."""
149 import inspect
150
151 contract: Dict[ str , Any] = {
152 "api_version" : APPLICATION_SIGNALS_API_VERSION ,
153 "encoding" : "python3 scripts/cloudwatch/di_instrumentation.py <op> --json-file args.json" ,
154 "region" : (
155 "pass --region, or set AWS_REGION/AWS_DEFAULT_REGION (default us-east-1); "
156 "use the region your instrumented service runs in"
157 ),
158 "ops" : {},
159 }
160 for op in _OPS :
161 fn = _resolve_tool(op)
162 sig = inspect.signature(fn)
163 args: Dict[ str , Any] = {}
164 for name, p in sig.parameters.items():
165 required = p.default is inspect.Parameter.empty
166 args[name] = { "required" : required}
167 if not required and p.default is not None :
168 args[name][ "default" ] = p.default
169 if name in _ARG_HINTS :
170 args[name].update( _ARG_HINTS [name])
171 contract[ "ops" ][op] = { "args" : args}
172 print (json.dumps(contract, indent = 2 , default = str ))
173 return 0
174
175
176 def _read_payload (ap, json_text: str | None , json_file: str | None ) -> dict :
177 """Resolve the op's JSON-object argument from --json-file, --json - (stdin), or --json.
178
179 Preferring a file or stdin keeps caller/source-derived values off the shell command line
180 (no quoting/injection surface). `ap.error` exits 2 on any malformed input.
181 """
182 sources = [s for s in (json_text is not None , json_file is not None ) if s]
183 if len (sources) > 1 :
184 ap.error( "pass the arguments via exactly one of --json or --json-file" )
185 if json_file is not None :
186 try :
187 raw = sys.stdin.read() if json_file == "-" else Path(json_file).read_text( "utf-8" )
188 except OSError as exc:
189 ap.error( f "--json-file could not be read: { exc } " )
190 elif json_text is not None :
191 raw = sys.stdin.read() if json_text == "-" else json_text
192 else :
193 ap.error(
194 "the op's arguments are required (use --json-file PATH, --json -, or --json '{...}')"
195 )
196 try :
197 payload = json.loads(raw)
198 except json.JSONDecodeError as exc:
199 ap.error( f "arguments are not valid JSON: { exc } " )
200 if not isinstance (payload, dict ):
201 ap.error( "arguments must be a JSON object of the op's parameters" )
202 return payload
203
204
205 def main (argv: list[ str ] | None = None ) -> int :
206 ap = argparse.ArgumentParser(
207 prog = "di_instrumentation.py" ,
208 description = "Host command for dynamic-instrumentation "
209 "instrumentation-config operations." ,
210 )
211 ap.add_argument( "op" , nargs = "?" , choices = sorted ( _OPS ), help = "instrumentation operation" )
212 ap.add_argument(
213 "--json" ,
214 dest = "json_payload" ,
215 help = "JSON object of the op's arguments (use '-' for stdin; prefer --json-file)" ,
216 )
217 ap.add_argument(
218 "--json-file" ,
219 dest = "json_file" ,
220 help = "read the op's JSON arguments from PATH (or '-' for stdin) — keeps values off "
221 "the shell command line" ,
222 )
223 ap.add_argument(
224 "--region" ,
225 help = "AWS region for the operation. Precedence: --region > AWS_REGION > "
226 "AWS_DEFAULT_REGION > us-east-1. AWS_PROFILE is used for credentials only; the "
227 "profile's region is ignored. Pass the region your instrumented service runs in." ,
228 )
229 ap.add_argument(
230 "--profile" ,
231 help = "AWS named profile for credentials (sets AWS_PROFILE for this call). If omitted, "
232 "the ambient default credential chain is used (env vars, shared profile, or IAM "
233 "role). Selects the account/identity; the profile's region is ignored (use --region). "
234 "Prefer IAM roles or SSO session credentials over long-lived access keys for these "
235 "live-service operations." ,
236 )
237 ap.add_argument(
238 "--print-contract" ,
239 action = "store_true" ,
240 help = "print the canonical op + arg schema (single source of truth) and exit" ,
241 )
242 args = ap.parse_args(argv)
243
244 if args.print_contract:
245 return _print_contract()
246 if not args.op:
247 ap.error( "an op is required (or use --print-contract)" )
248 # A --region flag is a thin front-end over the env-driven client builder: set AWS_REGION
249 # so get_application_signals_client()'s build_client() picks it up without threading
250 # region through every op signature and the gateway.
251 if args.region:
252 os.environ[ "AWS_REGION" ] = args.region
253 if args.profile:
254 os.environ[ "AWS_PROFILE" ] = args.profile
255 payload = _read_payload(ap, args.json_payload, args.json_file)
256
257 fn = _resolve_tool(args.op)
258 try :
259 result = fn( ** payload)
260 except TypeError as exc:
261 # Bad/unknown argument names for the op — deterministic input error.
262 print ( f "ERROR: invalid arguments for op ' { args.op } ': { exc } " , file = sys.stderr)
263 return 2
264 except RuntimeError as exc:
265 # The only deliberate RuntimeError in the op path is the SDK-too-old guard in
266 # get_application_signals_client(); surface its clean upgrade message instead of a
267 # bare traceback (the di_* op modules never raise — they return strings).
268 print ( f "ERROR: { exc } " , file = sys.stderr)
269 return 1
270 print (result.text)
271 return 0 if result.ok else 1
272
273
274 if __name__ == "__main__" :
275 raise SystemExit (main())