-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into update-test-suite
- Loading branch information
Showing
9 changed files
with
427 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
local-interchain/interchain/handlers/container_log_stream.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package handlers | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"unicode" | ||
|
||
dockertypes "github.com/docker/docker/api/types" | ||
dockerclient "github.com/docker/docker/client" | ||
"github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" | ||
"go.uber.org/zap" | ||
) | ||
|
||
var removeColorRegex = regexp.MustCompile("\x1b\\[[0-9;]*m") | ||
|
||
type ContainerStream struct { | ||
ctx context.Context | ||
logger *zap.Logger | ||
cli *dockerclient.Client | ||
authKey string | ||
testName string | ||
|
||
nameToID map[string]string | ||
} | ||
|
||
func NewContainerSteam(ctx context.Context, logger *zap.Logger, cli *dockerclient.Client, authKey, testName string, vals map[string][]*cosmos.ChainNode) *ContainerStream { | ||
nameToID := make(map[string]string) | ||
for _, nodes := range vals { | ||
for _, node := range nodes { | ||
nameToID[node.Name()] = node.ContainerID() | ||
} | ||
} | ||
|
||
return &ContainerStream{ | ||
ctx: ctx, | ||
authKey: authKey, | ||
cli: cli, | ||
logger: logger, | ||
testName: testName, | ||
nameToID: nameToID, | ||
} | ||
} | ||
|
||
func (cs *ContainerStream) StreamContainer(w http.ResponseWriter, r *http.Request) { | ||
if err := VerifyAuthKey(cs.authKey, r); err != nil { | ||
http.Error(w, err.Error(), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
containerID := r.URL.Query().Get("id") | ||
if containerID == "" { | ||
output := "No container ID provided. Available containers:\n" | ||
for name, id := range cs.nameToID { | ||
output += fmt.Sprintf("- %s: %s\n", name, id) | ||
} | ||
|
||
fmt.Fprint(w, output) | ||
fmt.Fprint(w, "Provide a container ID with ?id=<containerID>") | ||
return | ||
} | ||
|
||
// if container id is in the cs.nameToID map, use the mapped container ID | ||
if id, ok := cs.nameToID[containerID]; ok { | ||
containerID = id | ||
} else { | ||
fmt.Fprintf(w, "Container ID %s not found\n", containerID) | ||
return | ||
} | ||
|
||
// http://127.0.0.1:8080/container_logs?id=<ID>&colored=true | ||
isColored := strings.HasPrefix(strings.ToLower(r.URL.Query().Get("colored")), "t") | ||
tailLines := tailLinesParam(r.URL.Query().Get("lines")) | ||
|
||
rr, err := cs.cli.ContainerLogs(cs.ctx, containerID, dockertypes.ContainerLogsOptions{ | ||
ShowStdout: true, | ||
ShowStderr: true, | ||
Follow: true, | ||
Details: true, | ||
Tail: strconv.FormatUint(tailLines, 10), | ||
}) | ||
if err != nil { | ||
http.Error(w, "Unable to get container logs", http.StatusInternalServerError) | ||
return | ||
} | ||
defer rr.Close() | ||
|
||
// Set headers to keep the connection open for SSE (Server-Sent Events) | ||
w.Header().Set("Content-Type", "text/event-stream") | ||
w.Header().Set("Cache-Control", "no-cache") | ||
w.Header().Set("Connection", "keep-alive") | ||
|
||
// Flush ensures data is sent to the client immediately | ||
flusher, ok := w.(http.Flusher) | ||
if !ok { | ||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
for { | ||
buf := make([]byte, 8*1024) | ||
n, err := rr.Read(buf) | ||
if err != nil { | ||
break | ||
} | ||
|
||
text := string(buf[:n]) | ||
if !isColored { | ||
text, err = removeAnsiColorCodesFromText(string(buf[:n])) | ||
if err != nil { | ||
http.Error(w, "Unable to remove ANSI color codes", http.StatusInternalServerError) | ||
return | ||
} | ||
} | ||
|
||
fmt.Fprint(w, cleanSpecialChars(text)) | ||
flusher.Flush() | ||
} | ||
} | ||
|
||
func tailLinesParam(tailInput string) uint64 { | ||
if tailInput == "" { | ||
return defaultTailLines | ||
} | ||
|
||
tailLines, err := strconv.ParseUint(tailInput, 10, 64) | ||
if err != nil { | ||
return defaultTailLines | ||
} | ||
|
||
return tailLines | ||
} | ||
|
||
func removeAnsiColorCodesFromText(text string) (string, error) { | ||
return removeColorRegex.ReplaceAllString(text, ""), nil | ||
} | ||
|
||
func cleanSpecialChars(text string) string { | ||
return strings.Map(func(r rune) rune { | ||
if r == '\n' { | ||
return r | ||
} | ||
|
||
if unicode.IsPrint(r) { | ||
return r | ||
} | ||
return -1 | ||
}, text) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package handlers | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"log" | ||
"net/http" | ||
"os" | ||
"strconv" | ||
"time" | ||
|
||
"go.uber.org/zap" | ||
) | ||
|
||
const defaultTailLines = 50 | ||
|
||
type LogStream struct { | ||
fName string | ||
authKey string | ||
logger *zap.Logger | ||
} | ||
|
||
func NewLogSteam(logger *zap.Logger, file string, authKey string) *LogStream { | ||
return &LogStream{ | ||
fName: file, | ||
authKey: authKey, | ||
logger: logger, | ||
} | ||
} | ||
|
||
func (ls *LogStream) StreamLogs(w http.ResponseWriter, r *http.Request) { | ||
if err := VerifyAuthKey(ls.authKey, r); err != nil { | ||
http.Error(w, err.Error(), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
// Set headers to keep the connection open for SSE (Server-Sent Events) | ||
w.Header().Set("Content-Type", "text/event-stream") | ||
w.Header().Set("Cache-Control", "no-cache") | ||
w.Header().Set("Connection", "keep-alive") | ||
|
||
// Flush ensures data is sent to the client immediately | ||
flusher, ok := w.(http.Flusher) | ||
if !ok { | ||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
// Open the log file | ||
file, err := os.Open(ls.fName) | ||
if err != nil { | ||
http.Error(w, "Unable to open log file", http.StatusInternalServerError) | ||
return | ||
} | ||
defer file.Close() | ||
|
||
// Seek to the end of the file to read only new log entries | ||
file.Seek(0, io.SeekEnd) | ||
|
||
// Read new lines from the log file | ||
reader := bufio.NewReader(file) | ||
|
||
for { | ||
select { | ||
// In case client closes the connection, break out of loop | ||
case <-r.Context().Done(): | ||
return | ||
default: | ||
// Try to read a line | ||
line, err := reader.ReadString('\n') | ||
if err == nil { | ||
// Send the log line to the client | ||
fmt.Fprintf(w, "data: %s\n\n", line) | ||
flusher.Flush() // Send to client immediately | ||
} else { | ||
// If no new log is available, wait for a short period before retrying | ||
time.Sleep(100 * time.Millisecond) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (ls *LogStream) TailLogs(w http.ResponseWriter, r *http.Request) { | ||
if err := VerifyAuthKey(ls.authKey, r); err != nil { | ||
http.Error(w, err.Error(), http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
var linesToTail uint64 = defaultTailLines | ||
tailInput := r.URL.Query().Get("lines") | ||
if tailInput != "" { | ||
tailLines, err := strconv.ParseUint(tailInput, 10, 64) | ||
if err != nil { | ||
http.Error(w, "Invalid lines input", http.StatusBadRequest) | ||
return | ||
} | ||
linesToTail = tailLines | ||
} | ||
|
||
logs := TailFile(ls.logger, ls.fName, linesToTail) | ||
for _, log := range logs { | ||
fmt.Fprintf(w, "%s\n", log) | ||
} | ||
} | ||
|
||
func TailFile(logger *zap.Logger, logFile string, lines uint64) []string { | ||
// read the last n lines of a file | ||
file, err := os.Open(logFile) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer file.Close() | ||
|
||
totalLines, err := lineCounter(file) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
if lines > uint64(totalLines) { | ||
lines = uint64(totalLines) | ||
} | ||
|
||
file.Seek(0, io.SeekStart) | ||
reader := bufio.NewReader(file) | ||
|
||
var logs []string | ||
for i := 0; uint64(i) < totalLines-lines; i++ { | ||
_, _, err := reader.ReadLine() | ||
if err != nil { | ||
logger.Fatal("Error reading log file", zap.Error(err)) | ||
} | ||
} | ||
|
||
for { | ||
line, _, err := reader.ReadLine() | ||
if err == io.EOF { | ||
break | ||
} | ||
logs = append(logs, string(line)) | ||
} | ||
|
||
return logs | ||
} | ||
|
||
func lineCounter(r io.Reader) (uint64, error) { | ||
buf := make([]byte, 32*1024) | ||
var count uint64 = 0 | ||
lineSep := []byte{'\n'} | ||
|
||
for { | ||
c, err := r.Read(buf) | ||
count += uint64(bytes.Count(buf[:c], lineSep)) | ||
|
||
switch { | ||
case err == io.EOF: | ||
return count, nil | ||
|
||
case err != nil: | ||
return count, err | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.