ddapm-test-agent 1.35.0__py3-none-any.whl → 1.37.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.
ddapm_test_agent/agent.py CHANGED
@@ -39,7 +39,9 @@ from aiohttp.web import middleware
39
39
  from grpc import aio as grpc_aio
40
40
  from msgpack.exceptions import ExtraData as MsgPackExtraDataException
41
41
  from multidict import CIMultiDict
42
+ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceResponse
42
43
  from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import add_LogsServiceServicer_to_server
44
+ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceResponse
43
45
  from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import add_MetricsServiceServicer_to_server
44
46
 
45
47
  from . import _get_version
@@ -323,12 +325,15 @@ class Agent:
323
325
  "/v1.0/traces",
324
326
  "/v0.6/stats",
325
327
  "/v0.7/config",
328
+ "/info",
326
329
  "/telemetry/proxy/api/v2/apmtelemetry",
327
330
  "/v0.1/pipeline_stats",
328
331
  "/tracer_flare/v1",
329
332
  "/evp_proxy/v2/api/v2/llmobs",
330
333
  "/evp_proxy/v2/api/intake/llm-obs/v1/eval-metric",
331
334
  "/evp_proxy/v2/api/intake/llm-obs/v2/eval-metric",
335
+ "/evp_proxy/v2/api/v2/exposures",
336
+ "/evp_proxy/v4/api/v2/errorsintake",
332
337
  ]
333
338
 
334
339
  # Note that sessions are not cleared at any point since we don't know
@@ -721,7 +726,9 @@ class Agent:
721
726
  num_resource_logs,
722
727
  total_log_records,
723
728
  )
724
- return web.HTTPOk()
729
+ return web.Response(
730
+ body=ExportLogsServiceResponse().SerializeToString(), status=200, content_type="application/x-protobuf"
731
+ )
725
732
 
726
733
  async def handle_v1_metrics(self, request: Request) -> web.Response:
727
734
  metrics_data = self._decode_v1_metrics(request)
@@ -736,7 +743,9 @@ class Agent:
736
743
  num_resource_metrics,
737
744
  total_metrics,
738
745
  )
739
- return web.HTTPOk()
746
+ return web.Response(
747
+ body=ExportMetricsServiceResponse().SerializeToString(), status=200, content_type="application/x-protobuf"
748
+ )
740
749
 
741
750
  async def handle_v07_remoteconfig(self, request: Request) -> web.Response:
742
751
  """Emulates Remote Config endpoint: /v0.7/config"""
@@ -801,6 +810,12 @@ class Agent:
801
810
  async def handle_evp_proxy_v2_llmobs_eval_metric(self, request: Request) -> web.Response:
802
811
  return web.HTTPOk()
803
812
 
813
+ async def handle_evp_proxy_v2_api_v2_exposures(self, request: Request) -> web.Response:
814
+ return web.HTTPOk()
815
+
816
+ async def handle_evp_proxy_v4_api_v2_errorsintake(self, request: Request) -> web.Response:
817
+ return web.HTTPOk()
818
+
804
819
  async def handle_put_tested_integrations(self, request: Request) -> web.Response:
805
820
  # we need to store the request manually since this is not a real DD agent endpoint
806
821
  await self._store_request(request)
@@ -878,6 +893,7 @@ class Agent:
878
893
  "/v0.7/config",
879
894
  "/tracer_flare/v1",
880
895
  "/evp_proxy/v2/",
896
+ "/evp_proxy/v4/",
881
897
  ],
882
898
  "feature_flags": [],
883
899
  "config": {},
@@ -1124,6 +1140,8 @@ class Agent:
1124
1140
  self.handle_v1_tracer_flare,
1125
1141
  self.handle_evp_proxy_v2_api_v2_llmobs,
1126
1142
  self.handle_evp_proxy_v2_llmobs_eval_metric,
1143
+ self.handle_evp_proxy_v2_api_v2_exposures,
1144
+ self.handle_evp_proxy_v4_api_v2_errorsintake,
1127
1145
  self.handle_v1_logs,
1128
1146
  self.handle_v1_metrics,
1129
1147
  ):
@@ -1417,6 +1435,8 @@ class Agent:
1417
1435
  "/evp_proxy/v2/api/v2/llmobs": self.handle_evp_proxy_v2_api_v2_llmobs,
1418
1436
  "/evp_proxy/v2/api/intake/llm-obs/v1/eval-metric": self.handle_evp_proxy_v2_llmobs_eval_metric,
1419
1437
  "/evp_proxy/v2/api/intake/llm-obs/v2/eval-metric": self.handle_evp_proxy_v2_llmobs_eval_metric,
1438
+ "/evp_proxy/v2/api/v2/exposures": self.handle_evp_proxy_v2_api_v2_exposures,
1439
+ "/evp_proxy/v4/api/v2/errorsintake": self.handle_evp_proxy_v4_api_v2_errorsintake,
1420
1440
  "/info": self.handle_info,
1421
1441
  # Test endpoints
1422
1442
  "/test/session/start": self.handle_session_start,
@@ -1581,18 +1601,30 @@ def make_app(
1581
1601
  vcr_ci_mode: bool,
1582
1602
  vcr_provider_map: str,
1583
1603
  vcr_ignore_headers: str,
1604
+ enable_web_ui: bool = False,
1584
1605
  ) -> web.Application:
1585
1606
  agent = Agent()
1607
+
1608
+ # Build middleware list conditionally
1609
+ middlewares = []
1610
+ if enable_web_ui:
1611
+ from .web import request_response_capture_middleware
1612
+
1613
+ middlewares.append(request_response_capture_middleware)
1614
+ middlewares.extend(
1615
+ [
1616
+ handle_exception_middleware,
1617
+ agent.check_failure_middleware,
1618
+ agent.store_request_middleware,
1619
+ agent.request_forwarder_middleware,
1620
+ session_token_middleware,
1621
+ agent.vcr_proxy_suffix_middleware,
1622
+ ]
1623
+ )
1624
+
1586
1625
  app = web.Application(
1587
1626
  client_max_size=int(100e6), # 100MB - arbitrary
1588
- middlewares=[
1589
- handle_exception_middleware, # type: ignore
1590
- agent.check_failure_middleware, # type: ignore
1591
- agent.store_request_middleware, # type: ignore
1592
- agent.request_forwarder_middleware, # type: ignore
1593
- session_token_middleware, # type: ignore
1594
- agent.vcr_proxy_suffix_middleware, # type: ignore
1595
- ],
1627
+ middlewares=middlewares,
1596
1628
  )
1597
1629
  app.add_routes(
1598
1630
  [
@@ -1615,6 +1647,8 @@ def make_app(
1615
1647
  web.post("/evp_proxy/v2/api/v2/llmobs", agent.handle_evp_proxy_v2_api_v2_llmobs),
1616
1648
  web.post("/evp_proxy/v2/api/intake/llm-obs/v1/eval-metric", agent.handle_evp_proxy_v2_llmobs_eval_metric),
1617
1649
  web.post("/evp_proxy/v2/api/intake/llm-obs/v2/eval-metric", agent.handle_evp_proxy_v2_llmobs_eval_metric),
1650
+ web.post("/evp_proxy/v2/api/v2/exposures", agent.handle_evp_proxy_v2_api_v2_exposures),
1651
+ web.post("/evp_proxy/v4/api/v2/errorsintake", agent.handle_evp_proxy_v4_api_v2_errorsintake),
1618
1652
  web.get("/info", agent.handle_info),
1619
1653
  web.get("/test/session/start", agent.handle_session_start),
1620
1654
  web.get("/test/session/clear", agent.handle_session_clear),
@@ -1964,6 +1998,18 @@ def main(args: Optional[List[str]] = None) -> None:
1964
1998
  default=os.environ.get("VCR_IGNORE_HEADERS", ""),
1965
1999
  help="Comma-separated list of headers to ignore when recording VCR cassettes.",
1966
2000
  )
2001
+ parser.add_argument(
2002
+ "--web-ui-port",
2003
+ type=int,
2004
+ default=int(os.environ.get("WEB_UI_PORT", 0)),
2005
+ help="Port to serve the optional web UI (default: disabled). Example: --web-ui-port=8080",
2006
+ )
2007
+ parser.add_argument(
2008
+ "--max-requests",
2009
+ type=int,
2010
+ default=int(os.environ.get("MAX_REQUESTS", 200)),
2011
+ help="Maximum number of requests to keep in memory for the UI (default: 200). Older requests are discarded when limit is reached.",
2012
+ )
1967
2013
  parsed_args = parser.parse_args(args=args)
1968
2014
  logging.basicConfig(level=parsed_args.log_level)
1969
2015
 
@@ -2010,6 +2056,7 @@ def main(args: Optional[List[str]] = None) -> None:
2010
2056
  vcr_ci_mode=parsed_args.vcr_ci_mode,
2011
2057
  vcr_provider_map=parsed_args.vcr_provider_map,
2012
2058
  vcr_ignore_headers=parsed_args.vcr_ignore_headers,
2059
+ enable_web_ui=parsed_args.web_ui_port > 0,
2013
2060
  )
2014
2061
 
2015
2062
  # Validate port configuration
@@ -2019,6 +2066,13 @@ def main(args: Optional[List[str]] = None) -> None:
2019
2066
  raise ValueError("APM and OTLP GRPC ports cannot be the same")
2020
2067
  if parsed_args.otlp_http_port == parsed_args.otlp_grpc_port:
2021
2068
  raise ValueError("OTLP HTTP and GRPC ports cannot be the same")
2069
+ if parsed_args.web_ui_port > 0:
2070
+ if parsed_args.web_ui_port == parsed_args.port:
2071
+ raise ValueError("Web UI and APM ports cannot be the same")
2072
+ if parsed_args.web_ui_port == parsed_args.otlp_http_port:
2073
+ raise ValueError("Web UI and OTLP HTTP ports cannot be the same")
2074
+ if parsed_args.web_ui_port == parsed_args.otlp_grpc_port:
2075
+ raise ValueError("Web UI and OTLP GRPC ports cannot be the same")
2022
2076
 
2023
2077
  # Get the shared agent instance from the main app
2024
2078
  agent = app["agent"]
@@ -2036,15 +2090,41 @@ def main(args: Optional[List[str]] = None) -> None:
2036
2090
 
2037
2091
  otlp_http_app = make_otlp_http_app(agent)
2038
2092
 
2093
+ # Create Web UI app if enabled
2094
+ web_ui_app = None
2095
+ if parsed_args.web_ui_port > 0:
2096
+ from .web import WebUI
2097
+
2098
+ # Pass configuration directly to WebUI
2099
+ web_ui_config = {
2100
+ "snapshot_dir": parsed_args.snapshot_dir,
2101
+ "vcr_cassettes_directory": parsed_args.vcr_cassettes_directory,
2102
+ "disable_error_responses": parsed_args.disable_error_responses,
2103
+ "web_ui_port": parsed_args.web_ui_port,
2104
+ "max_requests": parsed_args.max_requests,
2105
+ }
2106
+ web_ui = WebUI(agent, config=web_ui_config)
2107
+ web_ui_app = web_ui.make_app()
2108
+ # Store WebUI instance reference for middleware access
2109
+ web_ui_app._webui_instance = web_ui
2110
+ # Also store on main app for middleware access
2111
+ app._webui_instance = web_ui
2112
+
2039
2113
  async def run_servers():
2040
2114
  """Run APM and OTLP HTTP servers concurrently."""
2041
- # Create runners for both apps
2115
+ # Create runners for apps
2042
2116
  apm_runner = web.AppRunner(app)
2043
2117
  await apm_runner.setup()
2044
2118
 
2045
2119
  otlp_http_runner = web.AppRunner(otlp_http_app)
2046
2120
  await otlp_http_runner.setup()
2047
2121
 
2122
+ # Create Web UI runner if enabled
2123
+ web_ui_runner = None
2124
+ if web_ui_app is not None:
2125
+ web_ui_runner = web.AppRunner(web_ui_app)
2126
+ await web_ui_runner.setup()
2127
+
2048
2128
  # Start GRPC server if available (async creation)
2049
2129
  otlp_grpc_server = await make_otlp_grpc_server_async(
2050
2130
  agent, parsed_args.otlp_http_port, parsed_args.otlp_grpc_port
@@ -2058,12 +2138,23 @@ def main(args: Optional[List[str]] = None) -> None:
2058
2138
 
2059
2139
  otlp_http_site = web.TCPSite(otlp_http_runner, port=parsed_args.otlp_http_port)
2060
2140
 
2061
- # Start both servers concurrently
2062
- await asyncio.gather(apm_site.start(), otlp_http_site.start())
2141
+ # Create Web UI site if enabled
2142
+ web_ui_site = None
2143
+ if web_ui_runner is not None:
2144
+ web_ui_site = web.TCPSite(web_ui_runner, port=parsed_args.web_ui_port)
2145
+
2146
+ # Start servers concurrently
2147
+ sites_to_start = [apm_site.start(), otlp_http_site.start()]
2148
+ if web_ui_site is not None:
2149
+ sites_to_start.append(web_ui_site.start())
2150
+
2151
+ await asyncio.gather(*sites_to_start)
2063
2152
 
2064
2153
  print(f"======== Running APM server on port {parsed_args.port} ========")
2065
2154
  print(f"======== Running OTLP HTTP server on port {parsed_args.otlp_http_port} ========")
2066
2155
  print(f"======== Running OTLP GRPC server on port {parsed_args.otlp_grpc_port} ========")
2156
+ if web_ui_site is not None:
2157
+ print(f"======== Running Web UI on port {parsed_args.web_ui_port} ========")
2067
2158
  print("(Press CTRL+C to quit)")
2068
2159
 
2069
2160
  try:
@@ -2074,6 +2165,8 @@ def main(args: Optional[List[str]] = None) -> None:
2074
2165
  finally:
2075
2166
  await apm_runner.cleanup()
2076
2167
  await otlp_http_runner.cleanup()
2168
+ if web_ui_runner is not None:
2169
+ await web_ui_runner.cleanup()
2077
2170
  await otlp_grpc_server.stop(grace=5.0)
2078
2171
 
2079
2172
  # Run the servers
@@ -26,7 +26,7 @@ class RemoteConfigServer:
26
26
  self._update_response(token, data)
27
27
 
28
28
  @staticmethod
29
- def _build_config_path_response(path: str, msg: str) -> Dict[str, Any]:
29
+ def _build_config_path_response(path: str, msg: Any) -> Dict[str, Any]:
30
30
  expires_date = datetime.datetime.strftime(
31
31
  datetime.datetime.now() + datetime.timedelta(days=1), "%Y-%m-%dT%H:%M:%SZ"
32
32
  )
@@ -84,7 +84,7 @@ class RemoteConfigServer:
84
84
  }
85
85
  return remote_config_payload
86
86
 
87
- def create_config_path_response(self, token: Optional[str], path: str, msg: str) -> None:
87
+ def create_config_path_response(self, token: Optional[str], path: str, msg: Any) -> None:
88
88
  remote_config_payload = self._build_config_path_response(path, msg)
89
89
  self.create_config_response(token, remote_config_payload)
90
90