125 lines
3.4 KiB
Go
125 lines
3.4 KiB
Go
package companion
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const logEventsPath = "/api/v1/game-client-bridge/companion/logs/events"
|
|
|
|
type LogEntry struct {
|
|
Seq uint64 `json:"seq"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Level string `json:"level,omitempty"`
|
|
Line string `json:"line"`
|
|
Fields map[string]string `json:"fields,omitempty"`
|
|
}
|
|
|
|
type LogStreamEvent struct {
|
|
ServerInstanceID string `json:"serverInstanceId"`
|
|
StreamID string `json:"streamId"`
|
|
Source string `json:"source"`
|
|
StreamKey string `json:"streamKey"`
|
|
LogSessionID string `json:"logSessionId,omitempty"`
|
|
SessionStartedAt time.Time `json:"sessionStartedAt,omitempty"`
|
|
LatestSeq uint64 `json:"latestSeq"`
|
|
Entry LogEntry `json:"entry"`
|
|
}
|
|
|
|
type logStreamRequest struct {
|
|
SessionToken string `json:"sessionToken"`
|
|
}
|
|
|
|
func (client *Client) StreamLogEvents(ctx context.Context, handle func(LogStreamEvent) error) error {
|
|
if handle == nil {
|
|
return fmt.Errorf("log event handler is required")
|
|
}
|
|
token, err := client.currentSession()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encoded, err := json.Marshal(logStreamRequest{SessionToken: token})
|
|
if err != nil {
|
|
return fmt.Errorf("encode log stream request: %w", err)
|
|
}
|
|
if len(encoded) > maxRequestBytes {
|
|
return fmt.Errorf("platform request exceeds the bounded payload size")
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.config.Platform.BaseURL+logEventsPath, bytes.NewReader(encoded))
|
|
if err != nil {
|
|
return fmt.Errorf("create log stream request: %w", err)
|
|
}
|
|
request.Header.Set("Accept", "text/event-stream")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response, err := client.httpClient.Do(request)
|
|
if err != nil {
|
|
return fmt.Errorf("open log stream: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
|
|
return HTTPError{StatusCode: response.StatusCode, ExpectedStatus: http.StatusOK}
|
|
}
|
|
return readLogEventStream(ctx, response.Body, handle)
|
|
}
|
|
|
|
func readLogEventStream(ctx context.Context, body io.Reader, handle func(LogStreamEvent) error) error {
|
|
reader := bufio.NewReader(body)
|
|
var eventName string
|
|
var dataLines []string
|
|
flush := func() error {
|
|
if len(dataLines) == 0 {
|
|
eventName = ""
|
|
return nil
|
|
}
|
|
name := eventName
|
|
if name == "" {
|
|
name = "message"
|
|
}
|
|
payload := strings.Join(dataLines, "\n")
|
|
eventName = ""
|
|
dataLines = nil
|
|
if name != "log" {
|
|
return nil
|
|
}
|
|
var event LogStreamEvent
|
|
if err := json.Unmarshal([]byte(payload), &event); err != nil {
|
|
return fmt.Errorf("decode log event: %w", err)
|
|
}
|
|
return handle(event)
|
|
}
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if len(line) > 0 {
|
|
line = strings.TrimRight(line, "\r\n")
|
|
switch {
|
|
case line == "":
|
|
if flushErr := flush(); flushErr != nil {
|
|
return flushErr
|
|
}
|
|
case strings.HasPrefix(line, ":"):
|
|
case strings.HasPrefix(line, "event:"):
|
|
eventName = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
|
case strings.HasPrefix(line, "data:"):
|
|
dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
|
|
}
|
|
}
|
|
if err != nil {
|
|
if ctx != nil && ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
if err == io.EOF {
|
|
return flush()
|
|
}
|
|
return fmt.Errorf("read log stream: %w", err)
|
|
}
|
|
}
|
|
}
|