Implement platform management features
This commit is contained in:
@@ -18,6 +18,7 @@ const (
|
||||
defaultLogEventHistoryLimit = 100
|
||||
maxLogEventHistoryLimit = 10000
|
||||
logEventHeartbeatInterval = 15 * time.Second
|
||||
managedLogSessionIDPrefix = "log-session:"
|
||||
)
|
||||
|
||||
// serverLogEvents streams platform-accepted server log history and live append events for the terminal drawer.
|
||||
@@ -26,7 +27,7 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
instance, streams, subscription, err := h.openLogEventSubscription(r)
|
||||
instance, streams, liveEligible, subscription, err := h.openLogEventSubscription(r)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
@@ -46,25 +47,17 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
historyLimit := parseLogEventHistoryLimit(r.URL.Query().Get("historyLimit"))
|
||||
for _, stream := range streams {
|
||||
if err := writeSSEJSON(w, "stream", "", dto.LogStreamFromDomain(stream)); err != nil {
|
||||
return
|
||||
}
|
||||
active := supervisedLogSession{}
|
||||
if liveEligible {
|
||||
active = activeSupervisedLogSession(streams)
|
||||
}
|
||||
if historyLimit > 0 {
|
||||
history, err := h.loadLogEventHistory(streams, historyLimit)
|
||||
if err != nil {
|
||||
_ = writeSSEJSON(w, "error", "", map[string]string{"message": err.Error()})
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
for _, event := range history {
|
||||
if err := writeSSEJSON(w, "log", logEventID(event), dto.LogStreamEventFromDomain(event)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
emittedThrough, err := h.writeCurrentLogSession(w, instance.ID, active, historyLimit)
|
||||
if err != nil {
|
||||
_ = writeSSEJSON(w, "error", "", map[string]string{"message": err.Error()})
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
if err := writeSSEJSON(w, "ready", "", dto.LogStreamEventsReadyResponse{ServerInstanceID: instance.ID, StreamCount: len(streams), ServerTime: time.Now().UTC()}); err != nil {
|
||||
if err := writeSSEJSON(w, "ready", "", dto.LogStreamEventsReadyResponse{ServerInstanceID: instance.ID, StreamCount: len(active.streams), ServerTime: time.Now().UTC()}); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
@@ -75,13 +68,85 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case event, ok := <-subscription.Events:
|
||||
case subscriptionEvent, ok := <-subscription.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if subscriptionEvent.Kind == service.LogEventSubscriptionEventProcessState {
|
||||
if subscriptionEvent.ServerInstanceID != instance.ID {
|
||||
continue
|
||||
}
|
||||
if subscriptionEvent.ProcessState != domain.ServerInstanceStateRunning {
|
||||
liveEligible = false
|
||||
if active.sessionID == "" {
|
||||
continue
|
||||
}
|
||||
active = supervisedLogSession{}
|
||||
emittedThrough, err = h.writeCurrentLogSession(w, instance.ID, active, historyLimit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
continue
|
||||
}
|
||||
streams, liveEligible, err = h.loadLiveLogSnapshot(instance.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
next := supervisedLogSession{}
|
||||
if liveEligible {
|
||||
next = activeSupervisedLogSession(streams)
|
||||
}
|
||||
if sameSupervisedLogSession(active, next) {
|
||||
continue
|
||||
}
|
||||
active = next
|
||||
emittedThrough, err = h.writeCurrentLogSession(w, instance.ID, active, historyLimit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
continue
|
||||
}
|
||||
if subscriptionEvent.Kind != service.LogEventSubscriptionEventLog || !liveEligible {
|
||||
continue
|
||||
}
|
||||
event := subscriptionEvent.LogEvent
|
||||
candidate := activeSupervisedLogSession([]domain.LogStream{event.Stream})
|
||||
if candidate.sessionID != "" && newerLogSession(candidate, active) {
|
||||
streams, liveEligible, err = h.loadLiveLogSnapshot(instance.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
next := supervisedLogSession{}
|
||||
if liveEligible {
|
||||
next = activeSupervisedLogSession(streams)
|
||||
}
|
||||
if !sameSupervisedLogSession(active, next) {
|
||||
active = next
|
||||
emittedThrough, err = h.writeCurrentLogSession(w, instance.ID, active, historyLimit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
if !active.contains(event.Stream) {
|
||||
continue
|
||||
}
|
||||
if !active.hasStream(event.Stream.ID) {
|
||||
active.streams = append(active.streams, event.Stream)
|
||||
if err := writeSSEJSON(w, "stream", "", dto.LogStreamFromDomain(event.Stream)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if event.Entry.Seq <= emittedThrough[event.Stream.ID] {
|
||||
continue
|
||||
}
|
||||
if err := writeSSEJSON(w, "log", logEventID(event), dto.LogStreamEventFromDomain(event)); err != nil {
|
||||
return
|
||||
}
|
||||
emittedThrough[event.Stream.ID] = event.Entry.Seq
|
||||
flusher.Flush()
|
||||
case <-heartbeat.C:
|
||||
if _, err := fmt.Fprintf(w, ": heartbeat %s\n\n", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
||||
@@ -92,34 +157,157 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *coreHandlers) openLogEventSubscription(r *http.Request) (domain.ServerInstance, []domain.LogStream, service.LogEventSubscription, error) {
|
||||
type supervisedLogSession struct {
|
||||
sessionID string
|
||||
startedAt time.Time
|
||||
streams []domain.LogStream
|
||||
}
|
||||
|
||||
func activeSupervisedLogSession(streams []domain.LogStream) supervisedLogSession {
|
||||
active := supervisedLogSession{}
|
||||
for _, stream := range streams {
|
||||
if stream.Source != domain.LogStreamSourceProcess || strings.TrimSpace(stream.LogSessionID) == "" || stream.SessionStartedAt.IsZero() {
|
||||
continue
|
||||
}
|
||||
candidate := supervisedLogSession{sessionID: stream.LogSessionID, startedAt: stream.SessionStartedAt}
|
||||
if active.sessionID == "" || newerLogSession(candidate, active) {
|
||||
active = candidate
|
||||
}
|
||||
}
|
||||
if active.sessionID == "" {
|
||||
return active
|
||||
}
|
||||
for _, stream := range streams {
|
||||
if stream.Source == domain.LogStreamSourceProcess && stream.LogSessionID == active.sessionID && stream.SessionStartedAt.Equal(active.startedAt) {
|
||||
active.streams = append(active.streams, stream)
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
func newerLogSession(candidate supervisedLogSession, current supervisedLogSession) bool {
|
||||
if candidate.sessionID == "" || candidate.sessionID == current.sessionID {
|
||||
return false
|
||||
}
|
||||
if current.sessionID == "" {
|
||||
return true
|
||||
}
|
||||
if !candidate.startedAt.Equal(current.startedAt) {
|
||||
return candidate.startedAt.After(current.startedAt)
|
||||
}
|
||||
return candidate.sessionID > current.sessionID
|
||||
}
|
||||
|
||||
func sameSupervisedLogSession(left supervisedLogSession, right supervisedLogSession) bool {
|
||||
return left.sessionID == right.sessionID && left.startedAt.Equal(right.startedAt)
|
||||
}
|
||||
|
||||
func (session supervisedLogSession) contains(stream domain.LogStream) bool {
|
||||
return session.sessionID != "" && stream.Source == domain.LogStreamSourceProcess && stream.LogSessionID == session.sessionID && stream.SessionStartedAt.Equal(session.startedAt)
|
||||
}
|
||||
|
||||
func (session supervisedLogSession) hasStream(streamID string) bool {
|
||||
for _, stream := range session.streams {
|
||||
if stream.ID == streamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *coreHandlers) writeCurrentLogSession(w http.ResponseWriter, serverInstanceID string, active supervisedLogSession, historyLimit int) (map[string]uint64, error) {
|
||||
emittedThrough := make(map[string]uint64, len(active.streams))
|
||||
if err := writeSSEJSON(w, "session", "", dto.LogStreamEventsSessionResponse{ServerInstanceID: serverInstanceID, LogSessionID: active.sessionID, SessionStartedAt: active.startedAt, StreamCount: len(active.streams), ServerTime: time.Now().UTC()}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, stream := range active.streams {
|
||||
if err := writeSSEJSON(w, "stream", "", dto.LogStreamFromDomain(stream)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if historyLimit == 0 || len(active.streams) == 0 {
|
||||
return emittedThrough, nil
|
||||
}
|
||||
history, err := h.loadLogEventHistory(active.streams, historyLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, event := range history {
|
||||
if err := writeSSEJSON(w, "log", logEventID(event), dto.LogStreamEventFromDomain(event)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if event.Entry.Seq > emittedThrough[event.Stream.ID] {
|
||||
emittedThrough[event.Stream.ID] = event.Entry.Seq
|
||||
}
|
||||
}
|
||||
return emittedThrough, nil
|
||||
}
|
||||
|
||||
func (h *coreHandlers) openLogEventSubscription(r *http.Request) (domain.ServerInstance, []domain.LogStream, bool, service.LogEventSubscription, error) {
|
||||
var instance domain.ServerInstance
|
||||
var streams []domain.LogStream
|
||||
var liveEligible bool
|
||||
var subscription service.LogEventSubscription
|
||||
var err error
|
||||
if h.enforceAuthorization {
|
||||
sessionID := bearerToken(r)
|
||||
instance, err = h.core.GetServerInstanceForSession(sessionID, r.PathValue("id"))
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, nil, subscription, err
|
||||
}
|
||||
streams, err = h.core.ListLogStreamsForSession(sessionID, domain.LogStreamFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, nil, subscription, err
|
||||
return domain.ServerInstance{}, nil, false, subscription, err
|
||||
}
|
||||
subscription, err = h.core.SubscribeLogEventsForSession(sessionID, instance.ID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, nil, false, subscription, err
|
||||
}
|
||||
} else {
|
||||
instance, err = h.core.GetServerInstance(r.PathValue("id"))
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, nil, subscription, err
|
||||
}
|
||||
streams, err = h.core.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, nil, subscription, err
|
||||
return domain.ServerInstance{}, nil, false, subscription, err
|
||||
}
|
||||
subscription, err = h.core.SubscribeLogEvents(instance.ID)
|
||||
if err != nil {
|
||||
return domain.ServerInstance{}, nil, false, subscription, err
|
||||
}
|
||||
}
|
||||
return instance, streams, subscription, err
|
||||
if err == nil {
|
||||
streams, liveEligible, err = h.loadLiveLogSnapshot(instance.ID)
|
||||
}
|
||||
if err != nil && subscription.Close != nil {
|
||||
subscription.Close()
|
||||
}
|
||||
return instance, streams, liveEligible, subscription, err
|
||||
}
|
||||
|
||||
func (h *coreHandlers) loadLiveLogSnapshot(serverInstanceID string) ([]domain.LogStream, bool, error) {
|
||||
instance, err := h.core.GetServerInstance(serverInstanceID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if instance.State != domain.ServerInstanceStateRunning || strings.TrimSpace(instance.RunEndpointID) == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
endpoint, err := h.core.GetRunEndpoint(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if endpoint.Status != domain.RunEndpointStatusOnline {
|
||||
return nil, false, nil
|
||||
}
|
||||
logSessionID := strings.TrimPrefix(instance.LifecycleProcessID, managedLogSessionIDPrefix)
|
||||
if logSessionID == instance.LifecycleProcessID || strings.TrimSpace(logSessionID) == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
streams, err := h.core.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
current := make([]domain.LogStream, 0, len(streams))
|
||||
for _, stream := range streams {
|
||||
if stream.Source == domain.LogStreamSourceProcess && stream.LogSessionID == logSessionID {
|
||||
current = append(current, stream)
|
||||
}
|
||||
}
|
||||
return current, true, nil
|
||||
}
|
||||
|
||||
func (h *coreHandlers) loadLogEventHistory(streams []domain.LogStream, limit int) ([]domain.LogStreamEvent, error) {
|
||||
|
||||
Reference in New Issue
Block a user