2121from operator import attrgetter
2222from typing import Dict , Optional
2323
24+ from dateutil import parser
2425from django .http import HttpResponse , StreamingHttpResponse
2526from django .shortcuts import get_object_or_404
27+ from django .utils import timezone
2628from django .utils .translation import gettext as _
2729from drf_yasg .utils import swagger_auto_schema
2830from rest_framework import status
2931from rest_framework .permissions import IsAuthenticated
3032from rest_framework .response import Response
3133from rest_framework .viewsets import GenericViewSet
34+ from urllib3 .exceptions import ReadTimeoutError
3235
3336from paas_wl .bk_app .applications .constants import WlAppType
3437from paas_wl .bk_app .applications .models import WlApp
4649 EventSerializer ,
4750 InstanceLogDownloadInputSLZ ,
4851 InstanceLogQueryInputSLZ ,
52+ InstanceLogStreamInputSLZ ,
4953 ListProcessesQuerySLZ ,
5054 ListWatcherRespSLZ ,
5155 ModuleScopedData ,
@@ -447,6 +451,9 @@ class InstanceManageViewSet(GenericViewSet, ApplicationCodeInPathMixin):
447451
448452 permission_classes = [IsAuthenticated , application_perm_class (AppAction .BASIC_DEVELOP )]
449453
454+ # Use special negotiation class to accept "text/event-stream" content type
455+ content_negotiation_class = IgnoreClientContentNegotiation
456+
450457 # 下载日志时,默认最大下载的行数一万
451458 download_log_lines_limit = 10000
452459
@@ -462,14 +469,21 @@ def retrieve_logs(self, request, code, module_name, environment, process_type, p
462469 try :
463470 logs = manager .get_instance_logs (
464471 process_type = process_type ,
472+ timestamps = True ,
465473 instance_name = process_instance_name ,
466474 previous = data ["previous" ],
467475 tail_lines = data ["tail_lines" ],
468476 )
469477 except InstanceNotFound :
470478 raise error_codes .PROCESS_INSTANCE_NOT_FOUND
471479
472- return Response (status = status .HTTP_200_OK , data = logs .splitlines ())
480+ # 拆分时间戳和具体日志, 用于开启流式日志时流畅过渡
481+ result_data = []
482+ for log in logs .splitlines ():
483+ rfc_timestamp , log_message = log .split (" " , 1 )
484+ result_data .append ({"timestamp" : rfc_timestamp , "message" : log_message })
485+
486+ return Response (status = status .HTTP_200_OK , data = result_data )
473487
474488 def download_logs (self , request , code , module_name , environment , process_type , process_instance_name ):
475489 """下载进程实例的日志"""
@@ -495,6 +509,50 @@ def download_logs(self, request, code, module_name, environment, process_type, p
495509 response ["Content-Disposition" ] = f'attachment; filename="{ code } -{ process_instance_name } -{ log_type } -logs.txt"'
496510 return response
497511
512+ def logs_stream (self , request , code , module_name , environment , process_type , process_instance_name ):
513+ """实时获取进程实例的日志"""
514+ env = self .get_env_via_path ()
515+ manager = ProcessManager (env )
516+
517+ slz = InstanceLogStreamInputSLZ (data = request .query_params )
518+ slz .is_valid (raise_exception = True )
519+ data = slz .validated_data
520+
521+ # 计算时间差(秒),并加1确保不会因为精度问题漏掉日志
522+ start_rfc_timestamp = data ["since_time" ]
523+ seconds_elapsed = int ((timezone .now () - parser .parse (start_rfc_timestamp )).total_seconds ()) + 1
524+
525+ def resp ():
526+ yield "event: ping\n "
527+ yield "data: \n \n "
528+
529+ try :
530+ for log_line in manager .get_instance_logs_stream (
531+ process_type = process_type ,
532+ instance_name = process_instance_name ,
533+ since_seconds = seconds_elapsed ,
534+ ):
535+ # log format: "2023-01-01T01:01:01.123456789Z ..."
536+ rfc_timestamp , log_message = log_line .split (" " , 1 )
537+ if rfc_timestamp <= start_rfc_timestamp :
538+ # 跳过早于请求时间的日志
539+ continue
540+
541+ data = json .dumps (
542+ {
543+ "timestamp" : rfc_timestamp ,
544+ "message" : log_message .rstrip ("\n " ),
545+ }
546+ )
547+ yield "event: message\n data: {}\n \n " .format (data )
548+ except ReadTimeoutError as e :
549+ logger .warning ("Log stream read timeout: %s" , e )
550+ finally :
551+ # 发送结束事件
552+ yield "event: EOF\n data: \n \n "
553+
554+ return StreamingHttpResponse (resp (), content_type = "text/event-stream" )
555+
498556 def restart (self , request , code , module_name , environment , process_instance_name ):
499557 """重启进程实例"""
500558 env = self .get_env_via_path ()
0 commit comments