add containerlog stream handling
This commit is contained in:
parent
08b2f5301f
commit
1c6b113e15
39
internal/containerlog/header.go
Normal file
39
internal/containerlog/header.go
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package containerlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
func Header(r io.Reader) (s StreamType, size uint64, err error) {
|
||||
var header []byte = make([]byte, 8)
|
||||
if _, err = io.ReadFull(r, header); err == nil {
|
||||
s = StreamType(header[0])
|
||||
if err = s.Validate(); err == nil {
|
||||
header[0] = 0x0
|
||||
size = binary.BigEndian.Uint64(header)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ReadMessage(r io.Reader, size uint64) (message string, err error) {
|
||||
var messageData []byte = make([]byte, size)
|
||||
var bytesRead int
|
||||
if bytesRead, err = r.Read(messageData); err == nil && uint64(bytesRead) == size {
|
||||
message = string(messageData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Read(r io.Reader) (s StreamType, message string, err error) {
|
||||
var messageSize uint64
|
||||
if s, messageSize, err = Header(r); err == nil {
|
||||
if message, err = ReadMessage(r, messageSize); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
28
internal/containerlog/header_test.go
Normal file
28
internal/containerlog/header_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package containerlog
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func TestLogHeader(t *testing.T) {
|
||||
for _, v := range []struct{ expected error; value []byte } {
|
||||
{ expected: nil, value: StreamStdout.Log("test message") },
|
||||
{ expected: nil, value: StreamStderr.Log("test error") },
|
||||
{ expected: ErrInvalidStreamType, value: StreamType(0x3).Log("fail") },
|
||||
{ expected: ErrInvalidStreamType, value: StreamType(0x4).Log("fail") },
|
||||
} {
|
||||
var buf bytes.Buffer
|
||||
_, e := buf.Write(v.value)
|
||||
assert.Nil(t, e)
|
||||
logType, logSize, err := Header(&buf)
|
||||
assert.ErrorIs(t, err, v.expected)
|
||||
assert.ErrorIs(t, logType.Validate(), v.expected)
|
||||
if err == nil {
|
||||
assert.Equal(t, uint64(len(v.value) - 8), logSize)
|
||||
}
|
||||
}
|
||||
}
|
74
internal/containerlog/streamreader.go
Normal file
74
internal/containerlog/streamreader.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package containerlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type StreamReader struct {
|
||||
source io.Reader
|
||||
streams []*ReadBuffer
|
||||
}
|
||||
|
||||
type ReadBuffer struct {
|
||||
streamtype StreamType
|
||||
reader *StreamReader
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func NewStreamReader(source io.Reader) (s *StreamReader) {
|
||||
s = &StreamReader{
|
||||
source: source,
|
||||
streams: make([]*ReadBuffer, 3),
|
||||
}
|
||||
s.streams[StreamStdout] = &ReadBuffer{
|
||||
streamtype: StreamStdout,
|
||||
reader: s,
|
||||
}
|
||||
s.streams[StreamStderr] = &ReadBuffer{
|
||||
streamtype: StreamStderr,
|
||||
reader: s,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StreamReader) StdoutPipe() io.ReadCloser {
|
||||
return s.streams[StreamStdout]
|
||||
}
|
||||
|
||||
func (s *StreamReader) StderrPipe() io.ReadCloser {
|
||||
return s.streams[StreamStderr]
|
||||
}
|
||||
|
||||
func (b *ReadBuffer) Read(p []byte) (n int, e error) {
|
||||
for {
|
||||
if b.reader.streams[b.streamtype].Len() >= len(p) {
|
||||
break
|
||||
}
|
||||
|
||||
streamtype, message, err := Read(b.reader.source)
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if b.reader.streams[streamtype] == nil {
|
||||
b.reader.streams[streamtype] = &ReadBuffer{
|
||||
streamtype: streamtype,
|
||||
reader: b.reader,
|
||||
}
|
||||
}
|
||||
|
||||
if bytesRead, bufferErr := b.reader.streams[streamtype].WriteString(message); bytesRead != len(message) || bufferErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return b.Buffer.Read(p)
|
||||
}
|
||||
|
||||
func (b *ReadBuffer) Close() error {
|
||||
return nil
|
||||
}
|
35
internal/containerlog/streamreader_test.go
Normal file
35
internal/containerlog/streamreader_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package containerlog
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
func TestStreamReader(t *testing.T) {
|
||||
|
||||
var logs bytes.Buffer
|
||||
logs.Write(StreamStdout.Log("stdout log message"))
|
||||
logs.Write(StreamStderr.Log("stderr log message"))
|
||||
logs.Write(StreamStdout.Log("stdout log message - line 2"))
|
||||
logs.Write(StreamStderr.Log("stderr log message - line 2"))
|
||||
logs.Write(StreamStderr.Log("stderr log message - line 3"))
|
||||
|
||||
sr := NewStreamReader(&logs)
|
||||
outpipe := sr.StdoutPipe()
|
||||
errpipe := sr.StderrPipe()
|
||||
|
||||
var message []byte = make([]byte, 20)
|
||||
n, ee := errpipe.Read(message)
|
||||
assert.Nil(t, ee)
|
||||
assert.Equal(t, 20, n)
|
||||
|
||||
assert.Equal(t, "stderr log messagest", string(message))
|
||||
|
||||
ov, oe := io.ReadAll(outpipe)
|
||||
assert.Nil(t, oe)
|
||||
assert.Equal(t, "stdout log messagestdout log message - line 2", string(ov))
|
||||
}
|
38
internal/containerlog/streamtype.go
Normal file
38
internal/containerlog/streamtype.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package containerlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type StreamType byte
|
||||
|
||||
const (
|
||||
StreamStdin StreamType = 0x0
|
||||
StreamStdout StreamType = 0x1
|
||||
StreamStderr StreamType = 0x2
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidStreamType error = errors.New("Invalid container log stream type")
|
||||
)
|
||||
|
||||
func (s StreamType) Validate() error {
|
||||
switch s {
|
||||
case StreamStdin, StreamStdout, StreamStderr:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%w: %d", ErrInvalidStreamType, s)
|
||||
}
|
||||
|
||||
func (s StreamType) Log(msg string) (log []byte) {
|
||||
msgLen := len(msg)
|
||||
log = make([]byte, 8 + msgLen)
|
||||
binary.BigEndian.PutUint64(log, uint64(msgLen))
|
||||
log[0] = byte(s)
|
||||
copy(log[8:], []byte(msg))
|
||||
return
|
||||
}
|
20
internal/containerlog/streamtype_test.go
Normal file
20
internal/containerlog/streamtype_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package containerlog
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStreamType(t *testing.T) {
|
||||
for _, v := range []struct{ expected error; value StreamType } {
|
||||
{ expected: nil, value: 0x0 },
|
||||
{ expected: nil, value: 0x1 },
|
||||
{ expected: nil, value: 0x2 },
|
||||
{ expected: ErrInvalidStreamType, value: 0x3 },
|
||||
{ expected: ErrInvalidStreamType, value: 0x4 },
|
||||
} {
|
||||
assert.ErrorIs(t, v.value.Validate(), v.expected)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user