55# file, You can obtain one at http://mozilla.org/MPL/2.0/.
66from __future__ import annotations
77
8- from typing import AsyncGenerator , AsyncIterable , ClassVar , Literal , Mapping , Union
8+ from typing import AsyncGenerator , ClassVar , Literal , Mapping , Union
99from enum import Enum
1010from json .decoder import JSONDecodeError
11- from time import time
1211from urllib .parse import quote as urllib_quote , urljoin as urllib_join
1312import asyncio
1413import inspect
1514import json
1615import logging
1716import platform
17+ import time
1818
19- from aiohttp import ClientSession , __version__ as aiohttp_version
19+ from aiohttp import ClientResponse , ClientSession , __version__ as aiohttp_version
2020from aiohttp .client_exceptions import ClientError , ContentTypeError
2121from yarl import URL
2222
@@ -239,7 +239,7 @@ async def _send(
239239 content : bytes | bytearray | str | AsyncBody ,
240240 query_params : dict [str , str ],
241241 headers : dict [str , str ],
242- ) -> JSON :
242+ ) -> tuple [ JSON , ClientResponse ] :
243243 request = self .session .request (
244244 str (method ), url , data = content , params = query_params , headers = headers
245245 )
@@ -258,7 +258,7 @@ async def _send(
258258 errcode = errcode ,
259259 message = message ,
260260 )
261- return await response .json ()
261+ return await response .json (), response
262262
263263 def _log_request (
264264 self ,
@@ -269,6 +269,7 @@ def _log_request(
269269 query_params : dict [str , str ],
270270 headers : dict [str , str ],
271271 req_id : int ,
272+ sensitive : bool ,
272273 ) -> None :
273274 if not self .log :
274275 return
@@ -277,26 +278,42 @@ def _log_request(
277278 elif inspect .isasyncgen (content ):
278279 size = headers .get ("Content-Length" , None )
279280 log_content = f"<{ size } async bytes>" if size else f"<stream with unknown length>"
281+ elif sensitive :
282+ log_content = f"<{ len (content )} sensitive bytes>"
280283 else :
281284 log_content = content
282285 as_user = query_params .get ("user_id" , None )
283- level = 1 if path == Path .v3 .sync else 5
286+ level = 5 if path == Path .v3 .sync else 10
284287 self .log .log (
285288 level ,
286- f"{ method } #{ req_id } /{ path } { log_content } " .strip (" " ),
289+ f"req #{ req_id } : { method } /{ path } { log_content } " .strip (" " ),
287290 extra = {
288291 "matrix_http_request" : {
289292 "req_id" : req_id ,
290293 "method" : str (method ),
291294 "path" : str (path ),
292295 "content" : (
293- orig_content if isinstance (orig_content , (dict , list )) else log_content
296+ orig_content
297+ if isinstance (orig_content , (dict , list )) and not sensitive
298+ else log_content
294299 ),
295300 "user" : as_user ,
296301 }
297302 },
298303 )
299304
305+ def _log_request_done (
306+ self , path : PathBuilder , req_id : int , duration : float , status : int
307+ ) -> None :
308+ level = 5 if path == Path .v3 .sync else 10
309+ duration_str = f"{ duration * 1000 :.1f} ms" if duration < 1 else f"{ duration :.3f} s"
310+ path_without_prefix = f"/{ path } " .replace ("/_matrix/client" , "" )
311+ self .log .log (
312+ level ,
313+ f"req #{ req_id } ({ path_without_prefix } ) completed in { duration_str } "
314+ f"with status { status } " ,
315+ )
316+
300317 def _full_path (self , path : PathBuilder | str ) -> str :
301318 path = str (path )
302319 if path and path [0 ] == "/" :
@@ -316,6 +333,7 @@ async def request(
316333 retry_count : int | None = None ,
317334 metrics_method : str = "" ,
318335 min_iter_size : int = 25 * 1024 * 1024 ,
336+ sensitive : bool = False ,
319337 ) -> JSON :
320338 """
321339 Make a raw Matrix API request.
@@ -335,6 +353,7 @@ async def request(
335353 min_iter_size: If the request body is larger than this value, it will be passed to
336354 aiohttp as an async iterable to stop it from copying the whole thing
337355 in memory.
356+ sensitive: If True, the request content will not be logged.
338357
339358 Returns:
340359 The parsed response JSON.
@@ -369,11 +388,18 @@ async def request(
369388 headers ["Content-Length" ] = str (len (content ))
370389 backoff = 4
371390 while True :
372- self ._log_request (method , path , content , orig_content , query_params , headers , req_id )
391+ self ._log_request (
392+ method , path , content , orig_content , query_params , headers , req_id , sensitive
393+ )
373394 API_CALLS .labels (method = metrics_method ).inc ()
374395 req_content = _async_iter_bytes (content ) if do_fake_iter else content
396+ start = time .monotonic ()
375397 try :
376- return await self ._send (method , full_url , req_content , query_params , headers or {})
398+ resp_data , resp = await self ._send (
399+ method , full_url , req_content , query_params , headers or {}
400+ )
401+ self ._log_request_done (path , req_id , time .monotonic () - start , resp .status )
402+ return resp_data
377403 except MatrixRequestError as e :
378404 API_CALLS_FAILED .labels (method = metrics_method ).inc ()
379405 if retry_count > 0 and e .http_status in (502 , 503 , 504 ):
@@ -382,6 +408,7 @@ async def request(
382408 f"retrying in { backoff } seconds"
383409 )
384410 else :
411+ self ._log_request_done (path , req_id , time .monotonic () - start , e .http_status )
385412 raise
386413 except ClientError as e :
387414 API_CALLS_FAILED .labels (method = metrics_method ).inc ()
@@ -401,7 +428,7 @@ async def request(
401428 def get_txn_id (self ) -> str :
402429 """Get a new unique transaction ID."""
403430 self .txn_id += 1
404- return f"mautrix-python_R{ self .txn_id } @T{ int (time () * 1000 )} "
431+ return f"mautrix-python_R{ self .txn_id } @T{ int (time . time () * 1000 )} "
405432
406433 def get_download_url (
407434 self ,
0 commit comments