From 99da4f1de917cdc2f419aa7aea3239eb13028c34 Mon Sep 17 00:00:00 2001 From: Joseph Sirianni Date: Wed, 19 May 2021 10:03:44 -0400 Subject: [PATCH 1/6] add dependabot (#305) --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..e51857593 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "gomod" + directory: "/internal/tools" + schedule: + interval: "monthly" From 2dc395aca8f62e655bbec20f519c6a5b01f7fab3 Mon Sep 17 00:00:00 2001 From: Joseph Sirianni Date: Thu, 20 May 2021 09:47:37 -0400 Subject: [PATCH 2/6] Tcp udp labels (#302) * add optional tcp connection labels to record * add optional udp connection labels to record * Added optional network metadata labels to tcp / udp operators --- CHANGELOG.md | 5 ++ docs/operators/tcp_input.md | 1 + docs/operators/udp_input.md | 1 + operator/builtin/input/tcp/tcp.go | 18 +++++++ operator/builtin/input/tcp/tcp_test.go | 66 +++++++++++++++++++++++ operator/builtin/input/udp/udp.go | 29 ++++++++--- operator/builtin/input/udp/udp_test.go | 72 ++++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d4887e7..cf2807eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.14.1] - Unreleased + +### Added +- Added optional network metadata labels to tcp / udp operators [PR302](https://github.com/observIQ/stanza/pull/302) + ## [0.14.0] - 2021-05-07 ### Added diff --git a/docs/operators/tcp_input.md b/docs/operators/tcp_input.md index aba939c2c..f8e56d9a8 100644 --- a/docs/operators/tcp_input.md +++ b/docs/operators/tcp_input.md @@ -14,6 +14,7 @@ The `tcp_input` operator listens for logs on one or more TCP connections. The op | `write_to` | $ | The record [field](/docs/types/field.md) written to when creating a new log entry | | `labels` | {} | A map of `key: value` labels to add to the entry's labels | | `resource` | {} | A map of `key: value` labels to add to the entry's resource | +| `add_labels` | false | Adds `net.transport`, `net.peer.ip`, `net.peer.port`, `net.host.ip` and `net.host.port` labels | #### TLS Configuration diff --git a/docs/operators/udp_input.md b/docs/operators/udp_input.md index 7b80f3405..417d25759 100644 --- a/docs/operators/udp_input.md +++ b/docs/operators/udp_input.md @@ -12,6 +12,7 @@ The `udp_input` operator listens for logs from UDP packets. | `write_to` | $ | The record [field](/docs/types/field.md) written to when creating a new log entry | | `labels` | {} | A map of `key: value` labels to add to the entry's labels | | `resource` | {} | A map of `key: value` labels to add to the entry's resource | +| `add_labels` | false | Adds `net.transport`, `net.peer.ip`, `net.peer.port`, `net.host.ip` and `net.host.port` labels | ### Example Configurations diff --git a/operator/builtin/input/tcp/tcp.go b/operator/builtin/input/tcp/tcp.go index 30c1cc090..3a3a1d934 100644 --- a/operator/builtin/input/tcp/tcp.go +++ b/operator/builtin/input/tcp/tcp.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "fmt" "net" + "strconv" "strings" "sync" "time" @@ -45,6 +46,7 @@ type TCPInputConfig struct { MaxBufferSize helper.ByteSize `json:"max_buffer_size,omitempty" yaml:"max_buffer_size,omitempty"` ListenAddress string `json:"listen_address,omitempty" yaml:"listen_address,omitempty"` TLS TLSConfig `json:"tls,omitempty" yaml:"tls,omitempty"` + AddLabels bool `json:"add_labels,omitempty" yaml:"add_labels,omitempty"` } // TLSConfig is the configuration for a TLS listener @@ -106,6 +108,7 @@ func (c TCPInputConfig) Build(context operator.BuildContext) ([]operator.Operato InputOperator: inputOperator, address: c.ListenAddress, maxBufferSize: int(c.MaxBufferSize), + addLabels: c.AddLabels, tlsEnable: c.TLS.Enable, tlsKeyPair: cert, backoff: backoff.Backoff{ @@ -123,6 +126,7 @@ type TCPInput struct { helper.InputOperator address string maxBufferSize int + addLabels bool tlsEnable bool tlsKeyPair tls.Certificate backoff backoff.Backoff @@ -228,6 +232,20 @@ func (t *TCPInput) goHandleMessages(ctx context.Context, conn net.Conn, cancel c t.Errorw("Failed to create entry", zap.Error(err)) continue } + + if t.addLabels { + entry.AddLabel("net.transport", "IP.TCP") + if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { + entry.AddLabel("net.peer.ip", addr.IP.String()) + entry.AddLabel("net.peer.port", strconv.FormatInt(int64(addr.Port), 10)) + } + + if addr, ok := conn.LocalAddr().(*net.TCPAddr); ok { + entry.AddLabel("net.host.ip", addr.IP.String()) + entry.AddLabel("net.host.port", strconv.FormatInt(int64(addr.Port), 10)) + } + } + t.Write(ctx, entry) } if err := scanner.Err(); err != nil { diff --git a/operator/builtin/input/tcp/tcp_test.go b/operator/builtin/input/tcp/tcp_test.go index 3d1e2ff0b..b9fe745de 100644 --- a/operator/builtin/input/tcp/tcp_test.go +++ b/operator/builtin/input/tcp/tcp_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "net" "os" + "strconv" "testing" "time" @@ -113,6 +114,66 @@ func tcpInputTest(input []byte, expected []string) func(t *testing.T) { } } +func tcpInputLabelsTest(input []byte, expected []string) func(t *testing.T) { + return func(t *testing.T) { + cfg := NewTCPInputConfig("test_id") + cfg.ListenAddress = ":0" + cfg.AddLabels = true + + ops, err := cfg.Build(testutil.NewBuildContext(t)) + require.NoError(t, err) + op := ops[0] + + mockOutput := testutil.Operator{} + tcpInput := op.(*TCPInput) + tcpInput.InputOperator.OutputOperators = []operator.Operator{&mockOutput} + + entryChan := make(chan *entry.Entry, 1) + mockOutput.On("Process", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + entryChan <- args.Get(1).(*entry.Entry) + }).Return(nil) + + err = tcpInput.Start() + require.NoError(t, err) + defer tcpInput.Stop() + + conn, err := net.Dial("tcp", tcpInput.listener.Addr().String()) + require.NoError(t, err) + defer conn.Close() + + _, err = conn.Write(input) + require.NoError(t, err) + + for _, expectedMessage := range expected { + select { + case entry := <-entryChan: + expectedLabels := map[string]string{ + "net.transport": "IP.TCP", + } + if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { + expectedLabels["net.host.ip"] = addr.IP.String() + expectedLabels["net.host.port"] = strconv.FormatInt(int64(addr.Port), 10) + } + if addr, ok := conn.LocalAddr().(*net.TCPAddr); ok { + expectedLabels["net.peer.ip"] = addr.IP.String() + expectedLabels["net.peer.port"] = strconv.FormatInt(int64(addr.Port), 10) + } + require.Equal(t, expectedMessage, entry.Record) + require.Equal(t, expectedLabels, entry.Labels) + case <-time.After(time.Second): + require.FailNow(t, "Timed out waiting for message to be written") + } + } + + select { + case entry := <-entryChan: + require.FailNow(t, "Unexpected entry: %s", entry) + case <-time.After(100 * time.Millisecond): + return + } + } +} + func tlsTCPInputTest(input []byte, expected []string) func(t *testing.T) { return func(t *testing.T) { @@ -280,6 +341,11 @@ func TestTcpInput(t *testing.T) { t.Run("CarriageReturn", tcpInputTest([]byte("message\r\n"), []string{"message"})) } +func TestTcpInputAattributes(t *testing.T) { + t.Run("Simple", tcpInputLabelsTest([]byte("message\n"), []string{"message"})) + t.Run("CarriageReturn", tcpInputLabelsTest([]byte("message\r\n"), []string{"message"})) +} + func TestTLSTcpInput(t *testing.T) { t.Run("Simple", tlsTCPInputTest([]byte("message\n"), []string{"message"})) t.Run("CarriageReturn", tlsTCPInputTest([]byte("message\r\n"), []string{"message"})) diff --git a/operator/builtin/input/udp/udp.go b/operator/builtin/input/udp/udp.go index 4e4c7d12a..68d050514 100644 --- a/operator/builtin/input/udp/udp.go +++ b/operator/builtin/input/udp/udp.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "strconv" "sync" "github.com/observiq/stanza/operator" @@ -27,6 +28,7 @@ type UDPInputConfig struct { helper.InputConfig `yaml:",inline"` ListenAddress string `json:"listen_address,omitempty" yaml:"listen_address,omitempty"` + AddLabels bool `json:"add_labels,omitempty" yaml:"add_labels,omitempty"` } // Build will build a udp input operator. @@ -49,6 +51,7 @@ func (c UDPInputConfig) Build(context operator.BuildContext) ([]operator.Operato InputOperator: inputOperator, address: address, buffer: make([]byte, 8192), + addLabels: c.AddLabels, } return []operator.Operator{udpInput}, nil } @@ -57,7 +60,8 @@ func (c UDPInputConfig) Build(context operator.BuildContext) ([]operator.Operato type UDPInput struct { buffer []byte helper.InputOperator - address *net.UDPAddr + address *net.UDPAddr + addLabels bool connection net.PacketConn cancel context.CancelFunc @@ -87,7 +91,7 @@ func (u *UDPInput) goHandleMessages(ctx context.Context) { defer u.wg.Done() for { - message, err := u.readMessage() + message, remoteAddr, err := u.readMessage() if err != nil { select { case <-ctx.Done(): @@ -104,23 +108,36 @@ func (u *UDPInput) goHandleMessages(ctx context.Context) { continue } + if u.addLabels { + entry.AddLabel("net.transport", "IP.UDP") + if addr, ok := u.connection.LocalAddr().(*net.UDPAddr); ok { + entry.AddLabel("net.host.ip", addr.IP.String()) + entry.AddLabel("net.host.port", strconv.FormatInt(int64(addr.Port), 10)) + } + + if addr, ok := remoteAddr.(*net.UDPAddr); ok { + entry.AddLabel("net.peer.ip", addr.IP.String()) + entry.AddLabel("net.peer.port", strconv.FormatInt(int64(addr.Port), 10)) + } + } + u.Write(ctx, entry) } }() } // readMessage will read log messages from the connection. -func (u *UDPInput) readMessage() (string, error) { - n, _, err := u.connection.ReadFrom(u.buffer) +func (u *UDPInput) readMessage() (string, net.Addr, error) { + n, addr, err := u.connection.ReadFrom(u.buffer) if err != nil { - return "", err + return "", nil, err } // Remove trailing characters and NULs for ; (n > 0) && (u.buffer[n-1] < 32); n-- { } - return string(u.buffer[:n]), nil + return string(u.buffer[:n]), addr, nil } // Stop will stop listening for udp messages. diff --git a/operator/builtin/input/udp/udp_test.go b/operator/builtin/input/udp/udp_test.go index fb7dd9eeb..cbdaa12cb 100644 --- a/operator/builtin/input/udp/udp_test.go +++ b/operator/builtin/input/udp/udp_test.go @@ -2,6 +2,7 @@ package udp import ( "net" + "strconv" "testing" "time" @@ -61,6 +62,70 @@ func udpInputTest(input []byte, expected []string) func(t *testing.T) { } } +func udpInputLabelsTest(input []byte, expected []string) func(t *testing.T) { + return func(t *testing.T) { + cfg := NewUDPInputConfig("test_input") + cfg.ListenAddress = ":0" + cfg.AddLabels = true + + ops, err := cfg.Build(testutil.NewBuildContext(t)) + require.NoError(t, err) + op := ops[0] + + mockOutput := testutil.Operator{} + udpInput, ok := op.(*UDPInput) + require.True(t, ok) + + udpInput.InputOperator.OutputOperators = []operator.Operator{&mockOutput} + + entryChan := make(chan *entry.Entry, 1) + mockOutput.On("Process", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + entryChan <- args.Get(1).(*entry.Entry) + }).Return(nil) + + err = udpInput.Start() + require.NoError(t, err) + defer udpInput.Stop() + + conn, err := net.Dial("udp", udpInput.connection.LocalAddr().String()) + require.NoError(t, err) + defer conn.Close() + + _, err = conn.Write(input) + require.NoError(t, err) + + for _, expectedRecord := range expected { + select { + case entry := <-entryChan: + expectedLabels := map[string]string{ + "net.transport": "IP.UDP", + } + // LocalAddr for udpInput.connection is a server address + if addr, ok := udpInput.connection.LocalAddr().(*net.UDPAddr); ok { + expectedLabels["net.host.ip"] = addr.IP.String() + expectedLabels["net.host.port"] = strconv.FormatInt(int64(addr.Port), 10) + } + // LocalAddr for conn is a client (peer) address + if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok { + expectedLabels["net.peer.ip"] = addr.IP.String() + expectedLabels["net.peer.port"] = strconv.FormatInt(int64(addr.Port), 10) + } + require.Equal(t, expectedRecord, entry.Record) + require.Equal(t, expectedLabels, entry.Labels) + case <-time.After(time.Second): + require.FailNow(t, "Timed out waiting for message to be written") + } + } + + select { + case entry := <-entryChan: + require.FailNow(t, "Unexpected entry: %s", entry) + case <-time.After(100 * time.Millisecond): + return + } + } +} + func TestUDPInput(t *testing.T) { t.Run("Simple", udpInputTest([]byte("message1"), []string{"message1"})) t.Run("TrailingNewlines", udpInputTest([]byte("message1\n"), []string{"message1"})) @@ -68,6 +133,13 @@ func TestUDPInput(t *testing.T) { t.Run("NewlineInMessage", udpInputTest([]byte("message1\nmessage2\n"), []string{"message1\nmessage2"})) } +func TestUDPInputLabels(t *testing.T) { + t.Run("Simple", udpInputLabelsTest([]byte("message1"), []string{"message1"})) + t.Run("TrailingNewlines", udpInputLabelsTest([]byte("message1\n"), []string{"message1"})) + t.Run("TrailingCRNewlines", udpInputLabelsTest([]byte("message1\r\n"), []string{"message1"})) + t.Run("NewlineInMessage", udpInputLabelsTest([]byte("message1\nmessage2\n"), []string{"message1\nmessage2"})) +} + func BenchmarkUdpInput(b *testing.B) { cfg := NewUDPInputConfig("test_id") cfg.ListenAddress = ":0" From f452f4866a85bc5c0f7f77fc66938aa2ac18bc63 Mon Sep 17 00:00:00 2001 From: Joseph Sirianni Date: Thu, 20 May 2021 10:27:45 -0400 Subject: [PATCH 3/6] changelog for 0.14.1 (#311) --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2807eb5..3c8ca65f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.14.1] - Unreleased +## [0.14.1] - 2021-05-20 ### Added - Added optional network metadata labels to tcp / udp operators [PR302](https://github.com/observIQ/stanza/pull/302) +- Added AWS Cloudwatch Logs input operator [PR289](https://github.com/observIQ/stanza/pull/289) ## [0.14.0] - 2021-05-07 From cd2cf8b7c1c90cd123ee3118024acb96c28f96e9 Mon Sep 17 00:00:00 2001 From: Joseph Sirianni Date: Thu, 20 May 2021 10:59:38 -0400 Subject: [PATCH 4/6] bump version 0.14.1 (#312) --- cmd/stanza/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/stanza/go.mod b/cmd/stanza/go.mod index 75ea7f74f..8766e6413 100644 --- a/cmd/stanza/go.mod +++ b/cmd/stanza/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/kardianos/service v1.2.0 - github.com/observiq/stanza v0.14.0 + github.com/observiq/stanza v0.14.1 github.com/observiq/stanza/operator/builtin/input/k8sevent v0.1.0 github.com/observiq/stanza/operator/builtin/input/windows v0.1.1 github.com/observiq/stanza/operator/builtin/output/elastic v0.1.2 From eda97c7f456b4d7e5de1c68e5c2115e836dc8bdd Mon Sep 17 00:00:00 2001 From: Daniel Jaglowski Date: Mon, 24 May 2021 11:10:42 -0400 Subject: [PATCH 5/6] Make buffer max chunk delay reconfigurable on the fly (#313) * Try making buffer max chunk delay reconfigurable on the fly * Make buffer max_chunk_size configurable on the fly * Make the MaxChunkDelay and MaxChunkSize available Co-authored-by: Andy Keller --- operator/buffer/buffer.go | 5 +++++ operator/buffer/disk.go | 30 ++++++++++++++++++++++++++++-- operator/buffer/memory.go | 29 +++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/operator/buffer/buffer.go b/operator/buffer/buffer.go index 5dd631e0c..ee1326f98 100644 --- a/operator/buffer/buffer.go +++ b/operator/buffer/buffer.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/observiq/stanza/entry" "github.com/observiq/stanza/operator" @@ -16,6 +17,10 @@ type Buffer interface { ReadWait(context.Context, []*entry.Entry) (Clearer, int, error) ReadChunk(context.Context) ([]*entry.Entry, Clearer, error) Close() error + MaxChunkDelay() time.Duration + MaxChunkSize() uint + SetMaxChunkDelay(time.Duration) + SetMaxChunkSize(uint) } // Config is a struct that wraps a Builder diff --git a/operator/buffer/disk.go b/operator/buffer/disk.go index a8c245fbc..94ea1e9d1 100644 --- a/operator/buffer/disk.go +++ b/operator/buffer/disk.go @@ -103,6 +103,8 @@ type DiskBuffer struct { maxChunkDelay time.Duration maxChunkSize uint + + reconfigMutex sync.RWMutex } // NewDiskBuffer creates a new DiskBuffer @@ -242,7 +244,7 @@ LOOP: // ReadChunk is a thin wrapper around ReadWait that simplifies the call at the expense of an extra allocation func (d *DiskBuffer) ReadChunk(ctx context.Context) ([]*entry.Entry, Clearer, error) { - entries := make([]*entry.Entry, d.maxChunkSize) + entries := make([]*entry.Entry, d.MaxChunkSize()) for { select { case <-ctx.Done(): @@ -250,7 +252,7 @@ func (d *DiskBuffer) ReadChunk(ctx context.Context) ([]*entry.Entry, Clearer, er default: } - ctx, cancel := context.WithTimeout(ctx, d.maxChunkDelay) + ctx, cancel := context.WithTimeout(ctx, d.MaxChunkDelay()) defer cancel() flushFunc, n, err := d.ReadWait(ctx, entries) if n > 0 { @@ -313,6 +315,30 @@ func (d *DiskBuffer) Read(dst []*entry.Entry) (f Clearer, i int, err error) { return d.newClearer(newRead), readCount, nil } +func (d *DiskBuffer) MaxChunkSize() uint { + d.reconfigMutex.RLock() + defer d.reconfigMutex.RUnlock() + return d.maxChunkSize +} + +func (d *DiskBuffer) MaxChunkDelay() time.Duration { + d.reconfigMutex.RLock() + defer d.reconfigMutex.RUnlock() + return d.maxChunkDelay +} + +func (d *DiskBuffer) SetMaxChunkSize(size uint) { + d.reconfigMutex.Lock() + d.maxChunkSize = size + d.reconfigMutex.Unlock() +} + +func (d *DiskBuffer) SetMaxChunkDelay(delay time.Duration) { + d.reconfigMutex.Lock() + d.maxChunkDelay = delay + d.reconfigMutex.Unlock() +} + // newFlushFunc returns a function that marks read entries as flushed func (d *DiskBuffer) newClearer(newRead []*readEntry) Clearer { return &diskClearer{ diff --git a/operator/buffer/memory.go b/operator/buffer/memory.go index a2b7b8211..3d0a24896 100644 --- a/operator/buffer/memory.go +++ b/operator/buffer/memory.go @@ -68,6 +68,7 @@ type MemoryBuffer struct { sem *semaphore.Weighted maxChunkDelay time.Duration maxChunkSize uint + reconfigMutex sync.RWMutex } // Add inserts an entry into the memory database, blocking until there is space @@ -105,7 +106,7 @@ func (m *MemoryBuffer) Read(dst []*entry.Entry) (Clearer, int, error) { // ReadChunk is a thin wrapper around ReadWait that simplifies the call at the expense of an extra allocation func (m *MemoryBuffer) ReadChunk(ctx context.Context) ([]*entry.Entry, Clearer, error) { - entries := make([]*entry.Entry, m.maxChunkSize) + entries := make([]*entry.Entry, m.MaxChunkSize()) for { select { case <-ctx.Done(): @@ -113,7 +114,7 @@ func (m *MemoryBuffer) ReadChunk(ctx context.Context) ([]*entry.Entry, Clearer, default: } - ctx, cancel := context.WithTimeout(ctx, m.maxChunkDelay) + ctx, cancel := context.WithTimeout(ctx, m.MaxChunkDelay()) defer cancel() flushFunc, n, err := m.ReadWait(ctx, entries) if n > 0 { @@ -145,6 +146,30 @@ func (m *MemoryBuffer) ReadWait(ctx context.Context, dst []*entry.Entry) (Cleare return m.newClearer(inFlightIDs[:i]), i, nil } +func (m *MemoryBuffer) MaxChunkSize() uint { + m.reconfigMutex.RLock() + defer m.reconfigMutex.RUnlock() + return m.maxChunkSize +} + +func (m *MemoryBuffer) MaxChunkDelay() time.Duration { + m.reconfigMutex.RLock() + defer m.reconfigMutex.RUnlock() + return m.maxChunkDelay +} + +func (m *MemoryBuffer) SetMaxChunkSize(size uint) { + m.reconfigMutex.Lock() + m.maxChunkSize = size + m.reconfigMutex.Unlock() +} + +func (m *MemoryBuffer) SetMaxChunkDelay(delay time.Duration) { + m.reconfigMutex.Lock() + m.maxChunkDelay = delay + m.reconfigMutex.Unlock() +} + type memoryClearer struct { buffer *MemoryBuffer ids []uint64 From 32e97bec1dc5a30f3d02507e632fdd252230dacb Mon Sep 17 00:00:00 2001 From: Joseph Sirianni Date: Mon, 24 May 2021 11:33:43 -0400 Subject: [PATCH 6/6] Changelog ahead of 0.14.2 release (#314) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8ca65f5..3136eee97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.14.2] - 2021-05-24 + +### Changed +- Make buffer max chunk delay reconfigurable on the fly [PR313](https://github.com/observIQ/stanza/pull/313) + ## [0.14.1] - 2021-05-20 ### Added