Coverage for src/fastapi_cloud_cli/commands/logs.py: 100%
101 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1import json 1abcdefg
2import logging 1abcdefg
3import re 1abcdefg
4from datetime import datetime 1abcdefg
5from pathlib import Path 1abcdefg
6from typing import Annotated 1abcdefg
8import typer 1abcdefg
9from httpx import HTTPError 1abcdefg
10from pydantic import BaseModel 1abcdefg
11from rich.markup import escape 1abcdefg
12from rich_toolkit import RichToolkit 1abcdefg
14from fastapi_cloud_cli._app import cloud_app 1abcdefg
15from fastapi_cloud_cli.api import ( 1abcdefg
16 APIClient,
17 AppLogEntry,
18 StreamLogError,
19 TooManyRetriesError,
20 get_http_error_code,
21 get_http_error_hint,
22 handle_http_error,
23)
24from fastapi_cloud_cli.commands._auth import UserCommand, get_user_command_context 1abcdefg
25from fastapi_cloud_cli.commands.apps._app import apps_app 1abcdefg
26from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail 1abcdefg
27from fastapi_cloud_cli.utils.cli import FastAPIRichToolkit 1abcdefg
28from fastapi_cloud_cli.utils.errors import ErrorCode 1abcdefg
29from fastapi_cloud_cli.utils.execution import JsonOutputOption 1abcdefg
31logger = logging.getLogger(__name__) 1abcdefg
34LOG_LEVEL_COLORS = { 1abcdefg
35 "debug": "blue",
36 "info": "cyan",
37 "warning": "yellow",
38 "warn": "yellow",
39 "error": "red",
40 "critical": "magenta",
41 "fatal": "magenta",
42}
44SINCE_PATTERN = re.compile(r"^\d+[smhd]$") 1abcdefg
45MIN_LOG_TAIL = 1 1abcdefg
46MAX_LOG_TAIL = 1000 1abcdefg
49class AppLogsOutput(BaseModel): 1abcdefg
50 app_id: str 1abcdefg
51 logs: list[AppLogEntry] 1abcdefg
54def _validate_since(value: str) -> str: 2a b @bc d [be f g ]b
55 """Validate the --since parameter format."""
56 if not SINCE_PATTERN.match(value): 2Eby xbZbz A B o + jbqbh = cbC Fb6b^b%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b_b'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b`b(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b{b)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b|b*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b}b+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b~b,b?bYb) * bb
57 raise typer.BadParameter( 2^b_b`b{b|b}b~b
58 "Invalid format. Use a number followed by s, m, h, or d (e.g., '5m', '1h', '2d')."
59 )
61 return value 2Eby xbZbz A B o + jbqbh = cbC Fb6b%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,b?bYb) * bb
64def _validate_tail(value: int) -> int: 2a b @bc d [be f g ]b
65 if not MIN_LOG_TAIL <= value <= MAX_LOG_TAIL: 2Eby xbZbz A B o + jbqbh = cbC Fb6bac%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bbc'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bcc(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bdc)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bec*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bfc+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bgc,b?bYb) * bb
66 raise typer.BadParameter( 2acbcccdcecfcgc
67 f"Invalid value. Use a number between {MIN_LOG_TAIL} and {MAX_LOG_TAIL}."
68 )
70 return value 2Eby xbZbz A B o + jbqbh = cbC Fb6b%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,b?bYb) * bb
73def _get_log_bullet(log: AppLogEntry) -> str: 1abcdefg
74 """Colored indicator rendered in the emoji bullet column.
76 ▕ draws at the right edge of its cell, centering the bar under the
77 double-width emojis.
78 """
79 color = LOG_LEVEL_COLORS.get(log.level.lower(), "dim") 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb
81 return f"[{color}]▕[/{color}]" 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb
84def _format_log_line(log: AppLogEntry) -> str: 1abcdefg
85 """Format a log entry for display"""
86 # Parse the timestamp string to format it consistently
87 timestamp = datetime.fromisoformat(log.timestamp.replace("Z", "+00:00")) 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb
88 timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb
90 return f"[dim]{timestamp_str}[/dim] {escape(log.message)}" 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb
93def _print_log_line(toolkit: RichToolkit, log: AppLogEntry) -> None: 1abcdefg
94 toolkit.print(_format_log_line(log), emoji=_get_log_bullet(log)) 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb
97def _render_app_logs_output(data: AppLogsOutput, toolkit: RichToolkit) -> None: 1abpcdsefgw
98 if not data.logs: 2Eby z A B C FbGbD E HbF G p H I IbJbJ K KbL M N O P LbMbQ R NbS T s U V ObPbW X QbY Z 0 1 2 RbSb3 4 Tb5 6 7 8 9 UbVb! # Wb$ % w ' ( XbYb) *
99 toolkit.print("No logs found for the specified time range.") 2EbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYb
100 return 2EbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYb
102 for log in data.logs: 1yzABCDEFGpHIJKLMNOPQRSTsUVWXYZ0123456789!#$%w'()*
103 _print_log_line(toolkit, log) 1yzABCDEFGpHIJKLMNOPQRSTsUVWXYZ0123456789!#$%w'()*
106def _print_app_log_json(app_id: str, log: AppLogEntry) -> None: 1abcdefg
107 typer.echo( 2xbybzbAbBbCbDb
108 json.dumps(
109 {
110 "type": "log",
111 "app_id": app_id,
112 **log.model_dump(mode="json"),
113 },
114 separators=(",", ":"),
115 )
116 )
119def _render_plain_error( 1abcdefg
120 toolkit: FastAPIRichToolkit,
121 *,
122 code: ErrorCode,
123 message: str,
124 hint: str,
125) -> None:
126 toolkit.print_error(message) 2o + jbqbh cbq , kbrbi dbr - lbsbj ebt . mbtbk fbu / nbubl gbv : obvbm hbx ; pbwbn ib
127 if hint: 2o + jbqbh cbq , kbrbi dbr - lbsbj ebt . mbtbk fbu / nbubl gbv : obvbm hbx ; pbwbn ib
128 toolkit.print_line() 2o qbh q rbi r sbj t tbk u ubl v vbm x wbn
129 toolkit.print_hint(hint) 2o qbh q rbi r sbj t tbk u ubl v vbm x wbn
132def _handle_stream_log_error( 1abcdefg
133 toolkit: FastAPIRichToolkit,
134 error: StreamLogError,
135) -> None:
136 hint: str | None = None 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib
138 if error.status_code == 404: 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib
139 code: ErrorCode = "not_found" 2jbkblbmbnbobpb
140 message = "App not found. Make sure to use the correct account." 2jbkblbmbnbobpb
142 elif isinstance(error.__cause__, HTTPError): 2o + h = cbq , i @ dbr - j ] ebt . k _ fbu / l { gbv : m } hbx ; n abib
143 code = get_http_error_code(error.__cause__) 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab
144 message = handle_http_error(error.__cause__) 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab
145 hint = get_http_error_hint(code) 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab
147 if error.status_code == 400 and hint is None: 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab
148 hint = ( 2h = i @ j ] k _ l { m } n ab
149 "Try a shorter time range (e.g. `--since 1d`). "
150 "Log retention depends on your plan."
151 )
153 else:
154 code = "api_error" 2cbdbebfbgbhbib
155 message = f"[red]Error:[/] {escape(str(error))}" 2cbdbebfbgbhbib
157 toolkit.fail(code, message, hint=hint, render_output=_render_plain_error) 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib
160def _process_log_stream( 1abcdefg
161 toolkit: FastAPIRichToolkit,
162 app_id: str,
163 tail: int,
164 since: str,
165 follow: bool,
166) -> None:
167 """Stream app logs and print them to the console."""
168 logs: list[AppLogEntry] = [] 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
170 try: 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
171 with APIClient() as client: 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
172 for log in client.stream_app_logs( 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
173 app_id=app_id,
174 tail=tail,
175 since=since,
176 follow=follow,
177 ):
178 if follow: 2y xbZbz A B C D E ? F yb0bG p H I J K [ L zb1bM N O P Q R ^ S Ab2bT s U V W X ` Y Bb3bZ 0 1 2 3 4 | 5 Cb4b6 7 8 9 ! # ~ $ Db5b% w ' ( ) * bb
179 if toolkit.mode == "json": 2xb? yb[ zb^ Ab` Bb| Cb~ Dbbb
180 _print_app_log_json(app_id, log) 2xbybzbAbBbCbDb
181 else:
182 _print_log_line(toolkit, log) 2? [ ^ ` | ~ bb
183 continue 2xb? yb[ zb^ Ab` Bb| Cb~ Dbbb
185 logs.append(log) 2y Zbz A B C D E F 0bG p H I J K L 1bM N O P Q R S 2bT s U V W X Y 3bZ 0 1 2 3 4 5 4b6 7 8 9 ! # $ 5b% w ' ( ) *
187 if not follow: 2Eby xbZbz A B C Fb6bGbD E ? HbF yb0bG p H I Ib7bJbJ K [ KbL zb1bM N O P Lb8bMbQ R ^ NbS Ab2bT s U V Ob9bPbW X ` QbY Bb3bZ 0 1 2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 9 Ub#bVb! # ~ Wb$ Db5b% w ' ( Xb$bYb) * bb
188 toolkit.success( 2Eby Zbz A B C FbGbD E HbF 0bG p H I IbJbJ K KbL 1bM N O P LbMbQ R NbS 2bT s U V ObPbW X QbY 3bZ 0 1 2 RbSb3 4 Tb5 4b6 7 8 9 UbVb! # Wb$ 5b% w ' ( XbYb) *
189 AppLogsOutput(app_id=app_id, logs=logs),
190 render_output=_render_app_logs_output,
191 )
192 return 2Eby xbZbz A B C Fb6bGbD E ? HbF yb0bG p H I Ib7bJbJ K [ KbL zb1bM N O P Lb8bMbQ R ^ NbS Ab2bT s U V Ob9bPbW X ` QbY Bb3bZ 0 1 2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 9 Ub#bVb! # ~ Wb$ Db5b% w ' ( Xb$bYb) * bb
193 except KeyboardInterrupt: # pragma: no cover 2o + jbqbh = cbq , kbrbi @ dbr - lbsbj ] ebt . mbtbk _ fbu / nbubl { gbv : obvbm } hbx ; pbwbn abib
194 toolkit.print_line()
196 return
197 except StreamLogError as e: 2o + jbqbh = cbq , kbrbi @ dbr - lbsbj ] ebt . mbtbk _ fbu / nbubl { gbv : obvbm } hbx ; pbwbn abib
198 _handle_stream_log_error(toolkit, e) 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib
199 except (TooManyRetriesError, TimeoutError): 2qbrbsbtbubvbwb
200 message = "Lost connection to log stream. Please try again later." 2qbrbsbtbubvbwb
201 toolkit.fail( 2qbrbsbtbubvbwb
202 "network_error",
203 message,
204 hint="Please try again later.",
205 render_output=_render_plain_error,
206 )
209@cloud_app.command(cls=UserCommand) 1abcdefg
210@apps_app.command("logs", cls=UserCommand) 1abcdefg
211def logs( 2a b @bc d [be f g ]b
212 ctx: typer.Context,
213 path: Annotated[
214 Path | None,
215 typer.Argument(
216 help=(
217 "Path to the directory with your app's pyproject.toml "
218 "(defaults to current directory)"
219 )
220 ),
221 ] = None,
222 app_id: Annotated[
223 str | None,
224 typer.Option(
225 "--app-id",
226 help="ID of the app whose logs should be fetched.",
227 ),
228 ] = None,
229 tail: int = typer.Option(
230 100,
231 "--tail",
232 "-t",
233 help=f"Number of log lines to show before streaming (max {MAX_LOG_TAIL}).",
234 show_default=True,
235 callback=_validate_tail,
236 ),
237 since: str = typer.Option(
238 "5m",
239 "--since",
240 "-s",
241 help=(
242 "Show logs since a specific time (e.g., '5m', '1h', '2d'). "
243 "Limited by your plan's log retention."
244 ),
245 show_default=True,
246 callback=_validate_since,
247 ),
248 follow: bool = typer.Option(
249 True,
250 "--follow/--no-follow",
251 "-f",
252 help="Stream logs in real-time (use --no-follow to fetch and exit).",
253 ),
254 json_output: JsonOutputOption = False,
255) -> None:
256 """Stream or fetch logs from your deployed app.
258 Examples:
259 fastapi cloud logs # Stream logs in real-time
260 fastapi cloud logs --no-follow # Fetch recent logs and exit
261 fastapi cloud logs --tail 50 --since 1h # Last 50 logs from the past hour
262 """
263 toolkit = get_user_command_context(ctx).toolkit 2Eby xbZbz A B o + jbqbh = cbC Fb6b%bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,bYb) * bb
265 target_app_id = resolve_app_id_or_fail( 2Eby xbZbz A B o + jbqbh = cbC Fb6b%bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,bYb) * bb
266 toolkit,
267 app_id=app_id,
268 path=path,
269 hint="Pass --app-id or run `fastapi cloud link` to link an app.",
270 )
272 logger.debug("Fetching logs for app ID: %s", target_app_id) 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
274 if follow: 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
275 toolkit.print( 2xb6b? yb7b[ zb8b^ Ab9b` Bb!b| Cb#b~ Db$bbb
276 f"Streaming logs for [bold]{target_app_id}[/bold] (Ctrl+C to exit)...",
277 emoji="📡",
278 )
279 else:
280 toolkit.print( 2Eby Zbz A B o + jbqbh = cbC FbGbD E HbF 0bG p H q , kbrbi @ dbI IbJbJ K KbL 1bM N O r - lbsbj ] ebP LbMbQ R NbS 2bT s U t . mbtbk _ fbV ObPbW X QbY 3bZ 0 1 u / nbubl { gb2 RbSb3 4 Tb5 4b6 7 8 v : obvbm } hb9 UbVb! # Wb$ 5b% w ' x ; pbwbn abib( XbYb) *
281 f"Fetching logs for [bold]{target_app_id}[/bold]...",
282 emoji="📜",
283 )
284 toolkit.print_line() 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
286 _process_log_stream( 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb
287 toolkit=toolkit,
288 app_id=target_app_id,
289 tail=tail,
290 since=since,
291 follow=follow,
292 )