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) } } }