2026-07-23 18:36:24 +08:00
package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/1024XEngineer/xinfra/server/internal/model"
"github.com/1024XEngineer/xinfra/server/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type TaskLogHandler struct {
db * gorm . DB
delivery * service . DeliveryService
wayne * service . WayneRoleBindingService
}
func NewTaskLogHandler ( db * gorm . DB , delivery * service . DeliveryService , wayne * service . WayneRoleBindingService ) * TaskLogHandler {
return & TaskLogHandler { db : db , delivery : delivery , wayne : wayne }
}
type taskLogSummary struct {
ID string `json:"id"`
Source string `json:"source"`
Service string `json:"service"`
Name string `json:"name"`
Runner string `json:"runner"`
Status string `json:"status"`
StatusText string `json:"status_text"`
StatusClass string `json:"status_class"`
BusinessLineID uint64 `json:"business_line_id"`
ReferenceID string `json:"reference_id"`
CreatedAt time . Time `json:"created_at"`
UpdatedAt time . Time `json:"updated_at"`
}
type taskLogLine struct {
Time string `json:"time"`
Message string `json:"message"`
Class string `json:"class"`
}
// List 聚合 AWX 交付任务和 Wayne 部署服务任务。
func ( h * TaskLogHandler ) List ( c * gin . Context ) {
claims , ok := CurrentClaims ( c )
if ! ok {
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "missing current user" })
return
}
source := strings . ToLower ( strings . TrimSpace ( c . DefaultQuery ( "source" , "all" )))
businessLineID , _ := strconv . ParseUint ( c . Query ( "business_line_id" ), 10 , 64 )
items := make ([] taskLogSummary , 0 )
if source == "" || source == "all" || source == "awx" {
awxItems , err := h . listAWXTasks ( c , claims . UserID , claims . IsAdmin , businessLineID )
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
items = append ( items , awxItems ... )
}
if source == "" || source == "all" || source == "wayne" {
wayneItems , err := h . listWayneTasks ( c , claims . UserID , claims . IsAdmin , businessLineID )
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
items = append ( items , wayneItems ... )
}
sortTaskLogSummaries ( items )
if len ( items ) > 100 {
items = items [: 100 ]
}
c . JSON ( http . StatusOK , gin . H { "items" : items })
}
func ( h * TaskLogHandler ) Get ( c * gin . Context ) {
claims , ok := CurrentClaims ( c )
if ! ok {
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "missing current user" })
return
}
id := c . Param ( "id" )
switch {
case strings . HasPrefix ( id , "awx:" ):
h . getAWXTask ( c , strings . TrimPrefix ( id , "awx:" ), claims . UserID , claims . IsAdmin )
case strings . HasPrefix ( id , "wayne:publish:" ):
h . getWayneTask ( c , id , claims . UserID , claims . IsAdmin )
default :
c . JSON ( http . StatusBadRequest , gin . H { "error" : "unknown task log source" })
}
}
func ( h * TaskLogHandler ) listAWXTasks ( c * gin . Context , userID uint64 , isAdmin bool , businessLineID uint64 ) ([] taskLogSummary , error ) {
2026-07-27 15:19:18 +08:00
tasks , err := h . delivery . ListTasks ( c . Request . Context (), userID , isAdmin , service . DeliveryTaskListFilter { BusinessLineID : businessLineID })
2026-07-23 18:36:24 +08:00
if err != nil {
return nil , err
}
items := make ([] taskLogSummary , 0 , len ( tasks ))
for _ , task := range tasks {
items = append ( items , awxTaskSummary ( task ))
}
return items , nil
}
func ( h * TaskLogHandler ) listWayneTasks ( c * gin . Context , userID uint64 , isAdmin bool , businessLineID uint64 ) ([] taskLogSummary , error ) {
namespaces , err := h . visibleWayneNamespaces ( c , userID , isAdmin , businessLineID )
if err != nil {
return nil , err
}
histories , err := h . wayne . ListDeploymentHistories ( c . Request . Context (), namespaces , 100 )
if err != nil {
return nil , err
}
items := make ([] taskLogSummary , 0 , len ( histories ))
for _ , history := range histories {
items = append ( items , wayneTaskSummary ( history ))
}
return items , nil
}
func ( h * TaskLogHandler ) getAWXTask ( c * gin . Context , taskID string , userID uint64 , isAdmin bool ) {
task , events , err := h . delivery . GetTask ( c . Request . Context (), taskID , userID , isAdmin )
if errors . Is ( err , gorm . ErrRecordNotFound ) {
c . JSON ( http . StatusNotFound , gin . H { "error" : "task log not found" })
return
}
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
lines := make ([] taskLogLine , 0 , len ( events ) + 16 )
for _ , event := range events {
lines = append ( lines , taskLogLine { Time : formatTaskLogTime ( event . CreatedAt ), Message : "[" + event . ToState + "] " + event . Message , Class : classForTaskStatus ( event . ToState )})
}
var execution model . ExecutionJob
2026-07-27 18:33:33 +08:00
if err := h . db . WithContext ( c . Request . Context ()). Where ( "task_id = ?" , task . ID ). First ( & execution ). Error ; err == nil && execution . ExecutorJobID != "" && execution . ExecutorJobID != "pending" && ! strings . HasPrefix ( execution . ExecutorJobID , "pending:" ) {
2026-07-23 18:36:24 +08:00
stdout , stdoutErr := h . delivery . AWXJobStdout ( c . Request . Context (), execution . ExecutorJobID )
if stdoutErr != nil {
lines = append ( lines , taskLogLine { Time : formatTaskLogTime ( time . Now ()), Message : "[awx] stdout fetch failed: " + stdoutErr . Error (), Class : "err" })
} else {
lines = append ( lines , splitStdoutLines ( stdout ) ... )
}
}
c . JSON ( http . StatusOK , gin . H { "task" : awxTaskSummary ( * task ), "lines" : lines })
}
func ( h * TaskLogHandler ) getWayneTask ( c * gin . Context , id string , userID uint64 , isAdmin bool ) {
resourceID , historyID , ok := parseWaynePublishTaskID ( id )
if ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "invalid Wayne task log id" })
return
}
namespaces , err := h . visibleWayneNamespaces ( c , userID , isAdmin , 0 )
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
history , err := h . wayne . GetDeploymentHistory ( c . Request . Context (), namespaces , resourceID , historyID )
if err != nil {
c . JSON ( http . StatusNotFound , gin . H { "error" : err . Error ()})
return
}
lines := [] taskLogLine {
{ Time : formatTaskLogTime ( history . CreatedAt ), Message : "[wayne] publish history #" + strconv . FormatInt ( history . ID , 10 ), Class : classForWaynePublishStatus ( history . Status )},
{ Time : formatTaskLogTime ( history . CreatedAt ), Message : "[deployment] " + history . ResourceName + " resource_id=" + strconv . FormatInt ( history . ResourceID , 10 ), Class : "" },
{ Time : formatTaskLogTime ( history . CreatedAt ), Message : "[cluster] " + history . Cluster + " template_id=" + strconv . FormatInt ( history . TemplateID , 10 ), Class : "" },
{ Time : formatTaskLogTime ( history . CreatedAt ), Message : "[user] " + history . User , Class : "" },
}
if strings . TrimSpace ( history . Message ) != "" {
lines = append ( lines , taskLogLine { Time : formatTaskLogTime ( history . CreatedAt ), Message : "[message] " + history . Message , Class : classForWaynePublishStatus ( history . Status )})
}
c . JSON ( http . StatusOK , gin . H { "task" : wayneTaskSummary ( * history ), "lines" : lines })
}
func awxTaskSummary ( task model . DeliveryTask ) taskLogSummary {
return taskLogSummary {
ID : "awx:" + task . ID ,
Source : "awx" ,
Service : "mysql" ,
Name : "MySQL 标准化交付 · " + task . InstanceName ,
Runner : "AWX Job Template #" + strconv . FormatUint ( task . TargetID , 10 ),
Status : task . Status ,
StatusText : textForTaskStatus ( task . Status ),
StatusClass : classForTaskStatus ( task . Status ),
BusinessLineID : task . BusinessLineID ,
ReferenceID : task . ID ,
CreatedAt : task . CreatedAt ,
UpdatedAt : task . UpdatedAt ,
}
}
func wayneTaskSummary ( history service . WayneDeploymentHistory ) taskLogSummary {
return taskLogSummary {
ID : waynePublishTaskID ( history ),
Source : "wayne" ,
Service : "wayne-deployment" ,
Name : wayneDeploymentTaskName ( history ),
Runner : "Wayne Native API" ,
Status : strconv . Itoa ( history . Status ),
StatusText : textForWaynePublishStatus ( history . Status ),
StatusClass : classForWaynePublishStatus ( history . Status ),
BusinessLineID : history . BusinessLineID ,
ReferenceID : strconv . FormatInt ( history . ID , 10 ),
CreatedAt : history . CreatedAt ,
UpdatedAt : history . CreatedAt ,
}
}
func ( h * TaskLogHandler ) visibleWayneNamespaces ( c * gin . Context , userID uint64 , isAdmin bool , businessLineID uint64 ) ([] model . BusinessLineWayneNamespace , error ) {
query := h . db . WithContext ( c . Request . Context ()). Order ( "business_line_id ASC, wayne_namespace_id ASC" )
if businessLineID != 0 {
query = query . Where ( "business_line_id = ?" , businessLineID )
}
if ! isAdmin {
query = query . Where ( "business_line_id IN (?)" , h . db . Model ( & model . BusinessLineUser {}). Select ( "business_line_id" ). Where ( "user_id = ?" , userID ))
}
var namespaces [] model . BusinessLineWayneNamespace
if err := query . Find ( & namespaces ). Error ; err != nil {
return nil , err
}
return namespaces , nil
}
func waynePublishTaskID ( history service . WayneDeploymentHistory ) string {
return "wayne:publish:" + strconv . FormatInt ( history . ResourceID , 10 ) + ":" + strconv . FormatInt ( history . ID , 10 )
}
func parseWaynePublishTaskID ( id string ) ( int64 , int64 , bool ) {
parts := strings . Split ( id , ":" )
if len ( parts ) != 4 || parts [ 0 ] != "wayne" || parts [ 1 ] != "publish" {
return 0 , 0 , false
}
resourceID , resourceErr := strconv . ParseInt ( parts [ 2 ], 10 , 64 )
historyID , historyErr := strconv . ParseInt ( parts [ 3 ], 10 , 64 )
if resourceErr != nil || historyErr != nil {
return 0 , 0 , false
}
return resourceID , historyID , true
}
func wayneDeploymentTaskName ( history service . WayneDeploymentHistory ) string {
name := strings . TrimSpace ( history . ResourceName )
if name == "" {
name = strconv . FormatInt ( history . ResourceID , 10 )
}
return "Wayne 服务部署 · " + name
}
func textForTaskStatus ( status string ) string {
switch status {
case model . TaskPending , model . TaskValidating , model . TaskDispatching :
return "等待"
case model . TaskRunning , model . TaskRegistering , model . TaskCanceling :
return "执行中"
case model . TaskFinished :
return "成功"
case model . TaskCanceled :
return "已取消"
default :
return "失败"
}
}
func classForTaskStatus ( status string ) string {
switch status {
case model . TaskFinished :
return "ok"
case model . TaskExecutionFailed , model . TaskValidationFailed , model . TaskRegisterFailed , model . TaskCanceled :
return "err"
case model . TaskRunning , model . TaskDispatching , model . TaskRegistering , model . TaskCanceling :
return "warn"
default :
return ""
}
}
func textForWaynePublishStatus ( status int ) string {
switch status {
case 1 :
return "成功"
case 0 :
return "失败"
default :
return "未知"
}
}
func classForWaynePublishStatus ( status int ) string {
switch status {
case 1 :
return "ok"
case 0 :
return "err"
default :
return ""
}
}
func splitStdoutLines ( stdout string ) [] taskLogLine {
lines := make ([] taskLogLine , 0 )
for _ , line := range strings . Split ( stdout , "\n" ) {
line = strings . TrimRight ( line , "\r" )
if strings . TrimSpace ( line ) == "" {
continue
}
lines = append ( lines , taskLogLine { Time : "" , Message : line , Class : classForOutputLine ( line )})
}
return lines
}
func classForOutputLine ( line string ) string {
lower := strings . ToLower ( line )
switch {
case strings . Contains ( lower , "failed" ) || strings . Contains ( lower , "fatal" ) || strings . Contains ( lower , "error" ):
return "err"
case strings . Contains ( lower , "ok:" ) || strings . Contains ( lower , "successful" ) || strings . Contains ( lower , "success" ):
return "ok"
case strings . Contains ( lower , "changed:" ):
return "tag-ok"
default :
return ""
}
}
func formatTaskLogTime ( t time . Time ) string {
if t . IsZero () {
return ""
}
return t . Format ( "15:04:05" )
}
func sortTaskLogSummaries ( items [] taskLogSummary ) {
for i := 1 ; i < len ( items ); i ++ {
item := items [ i ]
j := i - 1
for j >= 0 && items [ j ]. UpdatedAt . Before ( item . UpdatedAt ) {
items [ j + 1 ] = items [ j ]
j --
}
items [ j + 1 ] = item
}
}