@@ -160,6 +160,23 @@ class CloudWorkspaceResult(BaseModel):
160160 """ID of the organization this workspace belongs to."""
161161
162162
163+ class LogReadResult (BaseModel ):
164+ """Result of reading sync logs with pagination support."""
165+
166+ job_id : int
167+ """The job ID the logs belong to."""
168+ attempt_number : int
169+ """The attempt number the logs belong to."""
170+ log_text : str
171+ """The string containing the log text we are returning."""
172+ log_text_start_line : int
173+ """1-based line index of the first line returned."""
174+ log_text_line_count : int
175+ """Count of lines we are returning."""
176+ total_log_lines_available : int
177+ """Total number of log lines available, shows if any lines were missed due to the limit."""
178+
179+
163180def _get_cloud_workspace (workspace_id : str | None = None ) -> CloudWorkspace :
164181 """Get an authenticated CloudWorkspace.
165182
@@ -802,20 +819,67 @@ def get_cloud_sync_logs(
802819 default = None ,
803820 ),
804821 ],
805- ) -> str :
822+ max_lines : Annotated [
823+ int ,
824+ Field (
825+ description = (
826+ "Maximum number of lines to return. "
827+ "Defaults to 4000 if not specified. "
828+ "If '0' is provided, no limit is applied."
829+ ),
830+ default = 4000 ,
831+ ),
832+ ],
833+ from_tail : Annotated [
834+ bool | None ,
835+ Field (
836+ description = (
837+ "Pull from the end of the log text if total lines is greater than 'max_lines'. "
838+ "Defaults to True if `line_offset` is not specified. "
839+ "Cannot combine `from_tail=True` with `line_offset`."
840+ ),
841+ default = None ,
842+ ),
843+ ],
844+ line_offset : Annotated [
845+ int | None ,
846+ Field (
847+ description = (
848+ "Number of lines to skip from the beginning of the logs. "
849+ "Cannot be combined with `from_tail=True`."
850+ ),
851+ default = None ,
852+ ),
853+ ],
854+ ) -> LogReadResult :
806855 """Get the logs from a sync job attempt on Airbyte Cloud."""
856+ # Validate that line_offset and from_tail are not both set
857+ if line_offset is not None and from_tail :
858+ raise PyAirbyteInputError (
859+ message = "Cannot specify both 'line_offset' and 'from_tail' parameters." ,
860+ context = {"line_offset" : line_offset , "from_tail" : from_tail },
861+ )
862+
863+ if from_tail is None and line_offset is None :
864+ from_tail = True
807865 workspace : CloudWorkspace = _get_cloud_workspace (workspace_id )
808866 connection = workspace .get_connection (connection_id = connection_id )
809867
810868 sync_result : cloud .SyncResult | None = connection .get_sync_result (job_id = job_id )
811869
812870 if not sync_result :
813- return f"No sync job found for connection '{ connection_id } '"
871+ raise AirbyteMissingResourceError (
872+ resource_type = "sync job" ,
873+ resource_name_or_id = connection_id ,
874+ )
814875
815876 attempts = sync_result .get_attempts ()
816877
817878 if not attempts :
818- return f"No attempts found for job '{ sync_result .job_id } '"
879+ raise AirbyteMissingResourceError (
880+ resource_type = "sync attempt" ,
881+ resource_name_or_id = str (sync_result .job_id ),
882+ )
819883
820884 if attempt_number is not None :
821885 target_attempt = None
@@ -825,19 +889,52 @@ def get_cloud_sync_logs(
825889 break
826890
827891 if target_attempt is None :
828- return f"Attempt number { attempt_number } not found for job '{ sync_result .job_id } '"
892+ raise AirbyteMissingResourceError (
893+ resource_type = "sync attempt" ,
894+ resource_name_or_id = f"job { sync_result .job_id } , attempt { attempt_number } " ,
895+ )
829896 else :
830897 target_attempt = max (attempts , key = lambda a : a .attempt_number )
831898
832899 logs = target_attempt .get_full_log_text ()
833900
834901 if not logs :
835- return (
836- f"No logs available for job '{ sync_result .job_id } ', "
837- f"attempt { target_attempt .attempt_number } "
902+ # Return empty result with zero lines
903+ return LogReadResult (
904+ log_text = (
905+ f"[No logs available for job '{ sync_result .job_id } ', "
906+ f"attempt { target_attempt .attempt_number } .]"
907+ ),
908+ log_text_start_line = 1 ,
909+ log_text_line_count = 0 ,
910+ total_log_lines_available = 0 ,
911+ job_id = sync_result .job_id ,
912+ attempt_number = target_attempt .attempt_number ,
838913 )
839914
840- return logs
915+ # Apply line limiting
916+ log_lines = logs .splitlines ()
917+ total_lines = len (log_lines )
918+
919+ # Determine effective max_lines (0 means no limit)
920+ effective_max = total_lines if max_lines == 0 else max_lines
921+
922+ # Calculate start_index and slice based on from_tail or line_offset
923+ if from_tail :
924+ start_index = max (0 , total_lines - effective_max )
925+ selected_lines = log_lines [start_index :][:effective_max ]
926+ else :
927+ start_index = line_offset or 0
928+ selected_lines = log_lines [start_index : start_index + effective_max ]
929+
930+ return LogReadResult (
931+ log_text = "\n " .join (selected_lines ),
932+ log_text_start_line = start_index + 1 , # Convert to 1-based index
933+ log_text_line_count = len (selected_lines ),
934+ total_log_lines_available = total_lines ,
935+ job_id = sync_result .job_id ,
936+ attempt_number = target_attempt .attempt_number ,
937+ )
841938
842939
843940@mcp_tool (
0 commit comments