├── README.md ├── example └── main.go ├── cloudwatch_test.go ├── LICENSE ├── cloudwatch.go ├── reader.go ├── writer_test.go ├── writer.go └── reader_test.go /README.md: -------------------------------------------------------------------------------- 1 | This is a Go library to treat CloudWatch Log streams as io.Writers and io.Readers. 2 | 3 | 4 | ## Usage 5 | 6 | ```go 7 | group := NewGroup("group", cloudwatchlogs.New(defaults.DefaultConfig)) 8 | w, err := group.Create("stream") 9 | 10 | io.WriteString(w, "Hello World") 11 | 12 | r, err := group.Open("stream") 13 | io.Copy(os.Stdout, r) 14 | ``` 15 | 16 | ## Dependencies 17 | 18 | This library depends on [aws-sdk-go](https://github.com/aws/aws-sdk-go/). 19 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "time" 8 | "fmt" 9 | 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 12 | "github.com/ejholmes/cloudwatch" 13 | "github.com/pborman/uuid" 14 | ) 15 | 16 | func main() { 17 | sess := session.Must(session.NewSession()) 18 | 19 | g := cloudwatch.NewGroup("test", cloudwatchlogs.New(sess)) 20 | 21 | stream := uuid.New() 22 | 23 | w, err := g.Create(stream) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | r, err := g.Open(stream) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | go func() { 34 | var i int 35 | for { 36 | i++ 37 | <-time.After(time.Second / 30) 38 | _, err := fmt.Fprintf(w, "Line %d\n", i) 39 | if err != nil { 40 | log.Println(err) 41 | } 42 | } 43 | }() 44 | 45 | io.Copy(os.Stdout, r) 46 | } 47 | -------------------------------------------------------------------------------- /cloudwatch_test.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type mockClient struct { 9 | mock.Mock 10 | } 11 | 12 | func (c *mockClient) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { 13 | args := c.Called(input) 14 | return args.Get(0).(*cloudwatchlogs.PutLogEventsOutput), args.Error(1) 15 | } 16 | 17 | func (c *mockClient) CreateLogStream(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { 18 | args := c.Called(input) 19 | return args.Get(0).(*cloudwatchlogs.CreateLogStreamOutput), args.Error(1) 20 | } 21 | 22 | func (c *mockClient) GetLogEvents(input *cloudwatchlogs.GetLogEventsInput) (*cloudwatchlogs.GetLogEventsOutput, error) { 23 | args := c.Called(input) 24 | return args.Get(0).(*cloudwatchlogs.GetLogEventsOutput), args.Error(1) 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Eric Holmes 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /cloudwatch.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 8 | ) 9 | 10 | // Throttling and limits from http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html 11 | const ( 12 | // The maximum rate of a GetLogEvents request is 10 requests per second per AWS account. 13 | readThrottle = time.Second / 10 14 | 15 | // The maximum rate of a PutLogEvents request is 5 requests per second per log stream. 16 | writeThrottle = time.Second / 5 17 | ) 18 | 19 | // now is a function that returns the current time.Time. It's a variable so that 20 | // it can be stubbed out in unit tests. 21 | var now = time.Now 22 | 23 | // client duck types the aws sdk client for testing. 24 | type client interface { 25 | PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) 26 | CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) 27 | GetLogEvents(*cloudwatchlogs.GetLogEventsInput) (*cloudwatchlogs.GetLogEventsOutput, error) 28 | } 29 | 30 | // Group wraps a log stream group and provides factory methods for creating 31 | // readers and writers for streams. 32 | type Group struct { 33 | group string 34 | client *cloudwatchlogs.CloudWatchLogs 35 | } 36 | 37 | // NewGroup returns a new Group instance. 38 | func NewGroup(group string, client *cloudwatchlogs.CloudWatchLogs) *Group { 39 | return &Group{ 40 | group: group, 41 | client: client, 42 | } 43 | } 44 | 45 | // Create creates a log stream in the group and returns an io.Writer for it. 46 | func (g *Group) Create(stream string) (io.Writer, error) { 47 | if _, err := g.client.CreateLogStream(&cloudwatchlogs.CreateLogStreamInput{ 48 | LogGroupName: &g.group, 49 | LogStreamName: &stream, 50 | }); err != nil { 51 | return nil, err 52 | } 53 | 54 | return NewWriter(g.group, stream, g.client), nil 55 | } 56 | 57 | // Open returns an io.Reader to read from the log stream. 58 | func (g *Group) Open(stream string) (io.Reader, error) { 59 | return NewReader(g.group, stream, g.client), nil 60 | } 61 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 10 | ) 11 | 12 | // Reader is an io.Reader implementation that streams log lines from cloudwatch 13 | // logs. 14 | type Reader struct { 15 | group, stream, nextToken *string 16 | 17 | client client 18 | 19 | throttle <-chan time.Time 20 | 21 | b lockingBuffer 22 | 23 | // If an error occurs when getting events from the stream, this will be 24 | // populated and subsequent calls to Read will return the error. 25 | err error 26 | } 27 | 28 | func NewReader(group, stream string, client *cloudwatchlogs.CloudWatchLogs) *Reader { 29 | return newReader(group, stream, client) 30 | } 31 | 32 | func newReader(group, stream string, client client) *Reader { 33 | r := &Reader{ 34 | group: aws.String(group), 35 | stream: aws.String(stream), 36 | client: client, 37 | throttle: time.Tick(readThrottle), 38 | } 39 | go r.start() 40 | return r 41 | } 42 | 43 | func (r *Reader) start() { 44 | for { 45 | <-r.throttle 46 | if r.err = r.read(); r.err != nil { 47 | return 48 | } 49 | } 50 | } 51 | 52 | func (r *Reader) read() error { 53 | 54 | params := &cloudwatchlogs.GetLogEventsInput{ 55 | LogGroupName: r.group, 56 | LogStreamName: r.stream, 57 | StartFromHead: aws.Bool(true), 58 | NextToken: r.nextToken, 59 | } 60 | 61 | resp, err := r.client.GetLogEvents(params) 62 | 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // We want to re-use the existing token in the event that 68 | // NextForwardToken is nil, which means there's no new messages to 69 | // consume. 70 | if resp.NextForwardToken != nil { 71 | r.nextToken = resp.NextForwardToken 72 | } 73 | 74 | // If there are no messages, return so that the consumer can read again. 75 | if len(resp.Events) == 0 { 76 | return nil 77 | } 78 | 79 | for _, event := range resp.Events { 80 | r.b.WriteString(*event.Message) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (r *Reader) Read(b []byte) (int, error) { 87 | // Return the AWS error if there is one. 88 | if r.err != nil { 89 | return 0, r.err 90 | } 91 | 92 | // If there is not data right now, return. Reading from the buffer would 93 | // result in io.EOF being returned, which is not what we want. 94 | if r.b.Len() == 0 { 95 | return 0, nil 96 | } 97 | 98 | return r.b.Read(b) 99 | } 100 | 101 | // lockingBuffer is a bytes.Buffer that locks Reads and Writes. 102 | type lockingBuffer struct { 103 | sync.Mutex 104 | bytes.Buffer 105 | } 106 | 107 | func (r *lockingBuffer) Read(b []byte) (int, error) { 108 | r.Lock() 109 | defer r.Unlock() 110 | 111 | return r.Buffer.Read(b) 112 | } 113 | 114 | func (r *lockingBuffer) Write(b []byte) (int, error) { 115 | r.Lock() 116 | defer r.Unlock() 117 | 118 | return r.Buffer.Write(b) 119 | } 120 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func init() { 14 | now = func() time.Time { 15 | return time.Unix(1, 0) 16 | } 17 | } 18 | 19 | func TestWriter(t *testing.T) { 20 | c := new(mockClient) 21 | w := &Writer{ 22 | group: aws.String("group"), 23 | stream: aws.String("1234"), 24 | client: c, 25 | } 26 | 27 | c.On("PutLogEvents", &cloudwatchlogs.PutLogEventsInput{ 28 | LogEvents: []*cloudwatchlogs.InputLogEvent{ 29 | {Message: aws.String("Hello\n"), Timestamp: aws.Int64(1000)}, 30 | {Message: aws.String("World"), Timestamp: aws.Int64(1000)}, 31 | }, 32 | LogGroupName: aws.String("group"), 33 | LogStreamName: aws.String("1234"), 34 | }).Return(&cloudwatchlogs.PutLogEventsOutput{}, nil) 35 | 36 | n, err := io.WriteString(w, "Hello\nWorld") 37 | assert.NoError(t, err) 38 | assert.Equal(t, 11, n) 39 | 40 | err = w.Flush() 41 | assert.NoError(t, err) 42 | 43 | c.AssertExpectations(t) 44 | } 45 | 46 | func TestWriter_Rejected(t *testing.T) { 47 | c := new(mockClient) 48 | w := &Writer{ 49 | group: aws.String("group"), 50 | stream: aws.String("1234"), 51 | client: c, 52 | } 53 | 54 | c.On("PutLogEvents", &cloudwatchlogs.PutLogEventsInput{ 55 | LogEvents: []*cloudwatchlogs.InputLogEvent{ 56 | {Message: aws.String("Hello\n"), Timestamp: aws.Int64(1000)}, 57 | {Message: aws.String("World"), Timestamp: aws.Int64(1000)}, 58 | }, 59 | LogGroupName: aws.String("group"), 60 | LogStreamName: aws.String("1234"), 61 | }).Return(&cloudwatchlogs.PutLogEventsOutput{ 62 | RejectedLogEventsInfo: &cloudwatchlogs.RejectedLogEventsInfo{ 63 | TooOldLogEventEndIndex: aws.Int64(2), 64 | }, 65 | }, nil) 66 | 67 | _, err := io.WriteString(w, "Hello\nWorld") 68 | assert.NoError(t, err) 69 | 70 | err = w.Flush() 71 | assert.Error(t, err) 72 | assert.IsType(t, &RejectedLogEventsInfoError{}, err) 73 | 74 | _, err = io.WriteString(w, "Hello") 75 | assert.Error(t, err) 76 | 77 | c.AssertExpectations(t) 78 | } 79 | 80 | func TestWriter_NewLine(t *testing.T) { 81 | c := new(mockClient) 82 | w := &Writer{ 83 | group: aws.String("group"), 84 | stream: aws.String("1234"), 85 | client: c, 86 | } 87 | 88 | c.On("PutLogEvents", &cloudwatchlogs.PutLogEventsInput{ 89 | LogEvents: []*cloudwatchlogs.InputLogEvent{ 90 | {Message: aws.String("Hello\n"), Timestamp: aws.Int64(1000)}, 91 | }, 92 | LogGroupName: aws.String("group"), 93 | LogStreamName: aws.String("1234"), 94 | }).Return(&cloudwatchlogs.PutLogEventsOutput{}, nil) 95 | 96 | n, err := io.WriteString(w, "Hello\n") 97 | assert.NoError(t, err) 98 | assert.Equal(t, 6, n) 99 | 100 | err = w.Flush() 101 | assert.NoError(t, err) 102 | 103 | c.AssertExpectations(t) 104 | } 105 | 106 | func TestWriter_Close(t *testing.T) { 107 | c := new(mockClient) 108 | w := &Writer{ 109 | group: aws.String("group"), 110 | stream: aws.String("1234"), 111 | client: c, 112 | } 113 | 114 | c.On("PutLogEvents", &cloudwatchlogs.PutLogEventsInput{ 115 | LogEvents: []*cloudwatchlogs.InputLogEvent{ 116 | {Message: aws.String("Hello\n"), Timestamp: aws.Int64(1000)}, 117 | {Message: aws.String("World"), Timestamp: aws.Int64(1000)}, 118 | }, 119 | LogGroupName: aws.String("group"), 120 | LogStreamName: aws.String("1234"), 121 | }).Return(&cloudwatchlogs.PutLogEventsOutput{}, nil) 122 | 123 | n, err := io.WriteString(w, "Hello\nWorld") 124 | assert.NoError(t, err) 125 | assert.Equal(t, 11, n) 126 | 127 | err = w.Close() 128 | assert.NoError(t, err) 129 | 130 | n, err = io.WriteString(w, "Hello\nWorld") 131 | assert.Equal(t, io.ErrClosedPipe, err) 132 | 133 | c.AssertExpectations(t) 134 | } 135 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "sync" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 13 | ) 14 | 15 | type RejectedLogEventsInfoError struct { 16 | Info *cloudwatchlogs.RejectedLogEventsInfo 17 | } 18 | 19 | func (e *RejectedLogEventsInfoError) Error() string { 20 | return fmt.Sprintf("log messages were rejected") 21 | } 22 | 23 | // Writer is an io.Writer implementation that writes lines to a cloudwatch logs 24 | // stream. 25 | type Writer struct { 26 | group, stream, sequenceToken *string 27 | 28 | client client 29 | 30 | closed bool 31 | err error 32 | 33 | events eventsBuffer 34 | 35 | throttle <-chan time.Time 36 | 37 | sync.Mutex // This protects calls to flush. 38 | } 39 | 40 | func NewWriter(group, stream string, client *cloudwatchlogs.CloudWatchLogs) *Writer { 41 | w := &Writer{ 42 | group: aws.String(group), 43 | stream: aws.String(stream), 44 | client: client, 45 | throttle: time.Tick(writeThrottle), 46 | } 47 | go w.start() // start flushing 48 | return w 49 | } 50 | 51 | // Write takes b, and creates cloudwatch log events for each individual line. 52 | // If Flush returns an error, subsequent calls to Write will fail. 53 | func (w *Writer) Write(b []byte) (int, error) { 54 | if w.closed { 55 | return 0, io.ErrClosedPipe 56 | } 57 | 58 | if w.err != nil { 59 | return 0, w.err 60 | } 61 | 62 | return w.buffer(b) 63 | } 64 | 65 | // starts continously flushing the buffered events. 66 | func (w *Writer) start() error { 67 | for { 68 | // Exit if the stream is closed. 69 | if w.closed { 70 | return nil 71 | } 72 | 73 | <-w.throttle 74 | if err := w.Flush(); err != nil { 75 | return err 76 | } 77 | } 78 | } 79 | 80 | // Closes the writer. Any subsequent calls to Write will return 81 | // io.ErrClosedPipe. 82 | func (w *Writer) Close() error { 83 | w.closed = true 84 | return w.Flush() // Flush remaining buffer. 85 | } 86 | 87 | // Flush flushes the events that are currently buffered. 88 | func (w *Writer) Flush() error { 89 | w.Lock() 90 | defer w.Unlock() 91 | 92 | events := w.events.drain() 93 | 94 | // No events to flush. 95 | if len(events) == 0 { 96 | return nil 97 | } 98 | 99 | w.err = w.flush(events) 100 | return w.err 101 | } 102 | 103 | // flush flashes a slice of log events. This method should be called 104 | // sequentially to ensure that the sequence token is updated properly. 105 | func (w *Writer) flush(events []*cloudwatchlogs.InputLogEvent) error { 106 | resp, err := w.client.PutLogEvents(&cloudwatchlogs.PutLogEventsInput{ 107 | LogEvents: events, 108 | LogGroupName: w.group, 109 | LogStreamName: w.stream, 110 | SequenceToken: w.sequenceToken, 111 | }) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if resp.RejectedLogEventsInfo != nil { 117 | w.err = &RejectedLogEventsInfoError{Info: resp.RejectedLogEventsInfo} 118 | return w.err 119 | } 120 | 121 | w.sequenceToken = resp.NextSequenceToken 122 | 123 | return nil 124 | } 125 | 126 | // buffer splits up b into individual log events and inserts them into the 127 | // buffer. 128 | func (w *Writer) buffer(b []byte) (int, error) { 129 | r := bufio.NewReader(bytes.NewReader(b)) 130 | 131 | var ( 132 | n int 133 | eof bool 134 | ) 135 | 136 | for !eof { 137 | b, err := r.ReadBytes('\n') 138 | if err != nil { 139 | if err == io.EOF { 140 | eof = true 141 | } else { 142 | break 143 | } 144 | } 145 | 146 | if len(b) == 0 { 147 | continue 148 | } 149 | 150 | w.events.add(&cloudwatchlogs.InputLogEvent{ 151 | Message: aws.String(string(b)), 152 | Timestamp: aws.Int64(now().UnixNano() / 1000000), 153 | }) 154 | 155 | n += len(b) 156 | } 157 | 158 | return n, nil 159 | } 160 | 161 | // eventsBuffer represents a buffer of cloudwatch events that are protected by a 162 | // mutex. 163 | type eventsBuffer struct { 164 | sync.Mutex 165 | events []*cloudwatchlogs.InputLogEvent 166 | } 167 | 168 | func (b *eventsBuffer) add(event *cloudwatchlogs.InputLogEvent) { 169 | b.Lock() 170 | defer b.Unlock() 171 | 172 | b.events = append(b.events, event) 173 | } 174 | 175 | func (b *eventsBuffer) drain() []*cloudwatchlogs.InputLogEvent { 176 | b.Lock() 177 | defer b.Unlock() 178 | 179 | events := b.events[:] 180 | b.events = nil 181 | return events 182 | } 183 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestReader(t *testing.T) { 15 | c := new(mockClient) 16 | r := &Reader{ 17 | group: aws.String("group"), 18 | stream: aws.String("1234"), 19 | client: c, 20 | } 21 | 22 | c.On("GetLogEvents", &cloudwatchlogs.GetLogEventsInput{ 23 | LogGroupName: aws.String("group"), 24 | StartFromHead: aws.Bool(true), 25 | LogStreamName: aws.String("1234"), 26 | }).Once().Return(&cloudwatchlogs.GetLogEventsOutput{ 27 | Events: []*cloudwatchlogs.OutputLogEvent{ 28 | {Message: aws.String("Hello"), Timestamp: aws.Int64(1000)}, 29 | }, 30 | }, nil) 31 | 32 | err := r.read() 33 | assert.NoError(t, err) 34 | 35 | b := make([]byte, 1000) 36 | n, err := r.Read(b) 37 | assert.NoError(t, err) 38 | assert.Equal(t, 5, n) 39 | 40 | c.AssertExpectations(t) 41 | } 42 | 43 | func TestReader_Buffering(t *testing.T) { 44 | c := new(mockClient) 45 | r := &Reader{ 46 | group: aws.String("group"), 47 | stream: aws.String("1234"), 48 | client: c, 49 | } 50 | 51 | c.On("GetLogEvents", &cloudwatchlogs.GetLogEventsInput{ 52 | LogGroupName: aws.String("group"), 53 | StartFromHead: aws.Bool(true), 54 | LogStreamName: aws.String("1234"), 55 | }).Once().Return(&cloudwatchlogs.GetLogEventsOutput{ 56 | Events: []*cloudwatchlogs.OutputLogEvent{ 57 | {Message: aws.String("Hello"), Timestamp: aws.Int64(1000)}, 58 | }, 59 | }, nil) 60 | 61 | err := r.read() 62 | assert.NoError(t, err) 63 | 64 | b := make([]byte, 3) 65 | n, err := r.Read(b) //Hel 66 | assert.NoError(t, err) 67 | assert.Equal(t, 3, n) 68 | 69 | n, err = r.Read(b) //lo 70 | assert.NoError(t, err) 71 | assert.Equal(t, 2, n) 72 | 73 | c.AssertExpectations(t) 74 | } 75 | 76 | func TestReader_EndOfFile(t *testing.T) { 77 | c := new(mockClient) 78 | r := &Reader{ 79 | group: aws.String("group"), 80 | stream: aws.String("1234"), 81 | client: c, 82 | } 83 | 84 | c.On("GetLogEvents", &cloudwatchlogs.GetLogEventsInput{ 85 | LogGroupName: aws.String("group"), 86 | StartFromHead: aws.Bool(true), 87 | LogStreamName: aws.String("1234"), 88 | }).Once().Return(&cloudwatchlogs.GetLogEventsOutput{ 89 | Events: []*cloudwatchlogs.OutputLogEvent{ 90 | {Message: aws.String("Hello"), Timestamp: aws.Int64(1000)}, 91 | }, 92 | NextForwardToken: aws.String("next"), 93 | }, nil) 94 | 95 | c.On("GetLogEvents", &cloudwatchlogs.GetLogEventsInput{ 96 | LogGroupName: aws.String("group"), 97 | LogStreamName: aws.String("1234"), 98 | StartFromHead: aws.Bool(true), 99 | NextToken: aws.String("next"), 100 | }).Once().Return(&cloudwatchlogs.GetLogEventsOutput{ 101 | Events: []*cloudwatchlogs.OutputLogEvent{ 102 | {Message: aws.String("World"), Timestamp: aws.Int64(1000)}, 103 | }, 104 | }, nil) 105 | 106 | c.On("GetLogEvents", &cloudwatchlogs.GetLogEventsInput{ 107 | LogGroupName: aws.String("group"), 108 | LogStreamName: aws.String("1234"), 109 | StartFromHead: aws.Bool(true), 110 | NextToken: aws.String("next"), 111 | }).Once().Return(&cloudwatchlogs.GetLogEventsOutput{ 112 | Events: []*cloudwatchlogs.OutputLogEvent{}, 113 | }, nil) 114 | 115 | err := r.read() 116 | assert.NoError(t, err) 117 | 118 | b := make([]byte, 5) 119 | n, err := r.Read(b) //Hello 120 | assert.NoError(t, err) 121 | assert.Equal(t, 5, n) 122 | 123 | err = r.read() 124 | assert.NoError(t, err) 125 | 126 | n, err = r.Read(b) //World 127 | assert.NoError(t, err) 128 | assert.Equal(t, 5, n) 129 | 130 | err = r.read() 131 | assert.NoError(t, err) 132 | 133 | // Attempt to read more data, but there is none. 134 | n, err = r.Read(b) 135 | assert.NoError(t, err) 136 | assert.Equal(t, 0, n) 137 | 138 | c.AssertExpectations(t) 139 | } 140 | 141 | func TestReader_Err(t *testing.T) { 142 | c := new(mockClient) 143 | 144 | errBoom := errors.New("boom") 145 | c.On("GetLogEvents", &cloudwatchlogs.GetLogEventsInput{ 146 | LogGroupName: aws.String("group"), 147 | StartFromHead: aws.Bool(true), 148 | LogStreamName: aws.String("1234"), 149 | }).Once().Return(&cloudwatchlogs.GetLogEventsOutput{ 150 | Events: []*cloudwatchlogs.OutputLogEvent{ 151 | {Message: aws.String("Hello"), Timestamp: aws.Int64(1000)}, 152 | }, 153 | }, errBoom) 154 | 155 | r := newReader("group", "1234", c) 156 | 157 | b := new(bytes.Buffer) 158 | _, err := io.Copy(b, r) 159 | assert.Equal(t, errBoom, err) 160 | } 161 | --------------------------------------------------------------------------------