iop/apps/edge/internal/opsconsole/events.go
toki cd9d5830c7 feat: edge node registry, opsconsole, service 수정 및 agent-ops 스크립트 업데이트
- edge node registry: 노드 레지스트리 기능 개선
- edge opsconsole: 콘솔 및 이벤트 처리 로직 수정
- edge service: 서비스 레이어 변경
- agent-ops: init-agent-ops, sync-pull, sync-push 스킬 및 스크립트 업데이트
- 새 스크립트 추가: entry-files.sh
2026-05-19 10:16:41 +09:00

303 lines
7.1 KiB
Go

package opsconsole
import (
"context"
"fmt"
"io"
"strings"
"sync"
"go.uber.org/zap"
edgenode "iop/apps/edge/internal/node"
eventpkg "iop/packages/events"
iop "iop/proto/gen/iop"
)
// EventRouter renders run and node lifecycle events to the ops console.
// Foreground runs tracked via TrackForeground are suppressed from the
// asynchronous stream so the SendRun loop owns their output.
type EventRouter struct {
mu sync.Mutex
out io.Writer
registry *edgenode.Registry
logger *zap.Logger
foreground map[string]struct{}
streams map[string]*ResponseStream
nodeLabels map[string]string
aliasLabels map[string]string
}
func NewEventRouter(out io.Writer, registry *edgenode.Registry, logger *zap.Logger) *EventRouter {
return &EventRouter{
out: out,
registry: registry,
logger: logger,
foreground: make(map[string]struct{}),
streams: make(map[string]*ResponseStream),
nodeLabels: make(map[string]string),
aliasLabels: make(map[string]string),
}
}
func (r *EventRouter) TrackForeground(runID string) func() {
r.mu.Lock()
r.foreground[runID] = struct{}{}
r.mu.Unlock()
return func() {
r.mu.Lock()
delete(r.foreground, runID)
r.mu.Unlock()
}
}
func (r *EventRouter) Handle(event *iop.RunEvent) {
if event == nil || !event.GetBackground() {
return
}
runID := event.GetRunId()
r.mu.Lock()
if _, ok := r.foreground[runID]; ok {
r.mu.Unlock()
return
}
r.printAsyncLocked(event)
r.mu.Unlock()
}
func (r *EventRouter) HandleNodeEvent(event *iop.EdgeNodeEvent) {
r.mu.Lock()
r.printNodeEventLocked(event)
r.mu.Unlock()
}
func (r *EventRouter) Drain(ctx context.Context, runEvents <-chan *iop.RunEvent, nodeEvents <-chan *iop.EdgeNodeEvent) {
for {
select {
case <-ctx.Done():
return
case event, ok := <-runEvents:
if !ok {
runEvents = nil
} else {
r.Handle(event)
}
case event, ok := <-nodeEvents:
if !ok {
nodeEvents = nil
} else {
r.HandleNodeEvent(event)
}
}
if runEvents == nil && nodeEvents == nil {
return
}
}
}
func (r *EventRouter) NodeLabel(event *iop.RunEvent) string {
r.mu.Lock()
defer r.mu.Unlock()
return r.nodeLabelLocked(event)
}
func (r *EventRouter) nodeLabelLocked(event *iop.RunEvent) string {
nodeID := event.GetNodeId()
alias := event.GetNodeAlias()
return r.resolveNodeLabelLocked(nodeID, alias)
}
func (r *EventRouter) edgeNodeLabelLocked(event *iop.EdgeNodeEvent) string {
nodeID := event.GetNodeId()
alias := event.GetAlias()
return r.resolveNodeLabelLocked(nodeID, alias)
}
func (r *EventRouter) resolveNodeLabelLocked(nodeID, alias string) string {
if r.registry != nil {
if entry, ok := r.registry.Get(nodeID); ok {
return r.cacheEntryLabelLocked(entry)
}
if alias != "" {
if entry, err := r.registry.Resolve(alias); err == nil {
return r.cacheEntryLabelLocked(entry)
}
}
}
if nodeID != "" {
if label := r.nodeLabels[nodeID]; label != "" {
if alias != "" {
r.aliasLabels[alias] = label
}
return label
}
}
if alias != "" {
if label := r.aliasLabels[alias]; label != "" {
if nodeID != "" {
r.nodeLabels[nodeID] = label
}
return label
}
return alias
}
if nodeID != "" {
return nodeID
}
return "unknown"
}
func (r *EventRouter) cacheEntryLabelLocked(entry *edgenode.NodeEntry) string {
label := entry.DisplayLabel()
if entry.NodeID != "" {
r.nodeLabels[entry.NodeID] = label
}
if entry.Alias != "" {
r.aliasLabels[entry.Alias] = label
}
return label
}
func (r *EventRouter) printNodeEventLocked(event *iop.EdgeNodeEvent) {
if r.out == nil {
return
}
label := r.edgeNodeLabelLocked(event)
detail := transportCloseDetail(event.GetMetadata())
switch event.GetType() {
case eventpkg.TypeNodeConnected:
fmt.Fprintf(r.out, "%s connected reason=%q%s\n", nodeEventPrefix(label), event.GetReason(), detail)
case eventpkg.TypeNodeDisconnected:
fmt.Fprintf(r.out, "%s disconnected reason=%q%s\n", nodeEventPrefix(label), event.GetReason(), detail)
default:
fmt.Fprintf(r.out, "%s %s reason=%q%s\n", nodeEventPrefix(label), event.GetType(), event.GetReason(), detail)
}
}
func transportCloseDetail(metadata map[string]string) string {
if metadata == nil {
return ""
}
detail := ""
if reason := metadata[eventpkg.MetadataTransportCloseReason]; reason != "" {
detail += fmt.Sprintf(" transport_close_reason=%q", reason)
}
if err := metadata[eventpkg.MetadataTransportCloseError]; err != "" {
detail += fmt.Sprintf(" transport_close_error=%q", err)
}
return detail
}
func (r *EventRouter) printAsyncLocked(event *iop.RunEvent) {
if r.out == nil {
return
}
runID := event.GetRunId()
label := r.nodeLabelLocked(event)
switch event.GetType() {
case "start":
fmt.Fprintf(r.out, "%s start run_id=%s session=%s background=%v\n", nodeEventPrefix(label), runID, event.GetSessionId(), event.GetBackground())
case "delta":
r.responseStream(runID, label).Write(event.GetDelta())
case "complete":
r.finishStream(runID, label)
fmt.Fprintf(r.out, "%s complete run_id=%s detail=%q\n", nodeEventPrefix(label), runID, event.GetMessage())
case "cancelled":
r.finishStreamIfStarted(runID)
fmt.Fprintf(r.out, "%s cancelled run_id=%s\n", nodeEventPrefix(label), runID)
case "error":
r.finishStreamIfStarted(runID)
fmt.Fprintf(r.out, "%s error run_id=%s detail=%q\n", nodeEventPrefix(label), runID, event.GetError())
default:
fmt.Fprintf(r.out, "%s %s run_id=%s detail=%q\n", nodeEventPrefix(label), event.GetType(), runID, event.GetMessage())
}
}
func (r *EventRouter) responseStream(runID, label string) *ResponseStream {
if s, ok := r.streams[runID]; ok {
return s
}
s := NewResponseStream(r.out, nodeMessagePrefix(label))
r.streams[runID] = s
return s
}
func (r *EventRouter) finishStream(runID, label string) {
if s, ok := r.streams[runID]; ok {
s.Finish()
delete(r.streams, runID)
return
}
NewResponseStream(r.out, nodeMessagePrefix(label)).Finish()
}
func (r *EventRouter) finishStreamIfStarted(runID string) {
if s, ok := r.streams[runID]; ok {
s.FinishIfStarted()
delete(r.streams, runID)
}
}
type ResponseStream struct {
out io.Writer
prefix string
started bool
endedWithNewline bool
}
func NewResponseStream(out io.Writer, prefix string) *ResponseStream {
return &ResponseStream{out: out, prefix: prefix, endedWithNewline: true}
}
func nodeEventPrefix(label string) string {
return fmt.Sprintf("[%s-evt]", label)
}
func nodeMessagePrefix(label string) string {
return fmt.Sprintf("[%s-msg] ", label)
}
func (s *ResponseStream) Write(delta string) {
if s.out == nil || delta == "" {
return
}
for delta != "" {
if !s.started || s.endedWithNewline {
fmt.Fprint(s.out, s.prefix)
s.started = true
s.endedWithNewline = false
}
idx := strings.IndexByte(delta, '\n')
if idx < 0 {
fmt.Fprint(s.out, delta)
return
}
fmt.Fprint(s.out, delta[:idx+1])
s.endedWithNewline = true
delta = delta[idx+1:]
}
}
func (s *ResponseStream) Finish() {
if s.out == nil {
return
}
if !s.started {
fmt.Fprintf(s.out, "%s<empty>\n", s.prefix)
return
}
if !s.endedWithNewline {
fmt.Fprintln(s.out)
}
}
func (s *ResponseStream) FinishIfStarted() {
if s.started {
s.Finish()
}
}