Skip to content

Commit

Permalink
Merge pull request #398 from gliderlabs/master
Browse files Browse the repository at this point in the history
release 3.2.5
  • Loading branch information
michaelshobbs committed Jun 6, 2018
2 parents acfb302 + 3597569 commit c9a891c
Show file tree
Hide file tree
Showing 8 changed files with 520 additions and 5 deletions.
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ All notable changes to this project will be documented in this file.

### Changed

## [v3.2.4] - 2018-01-16
## [v3.2.5] - 2018-06-05
- @gmelika panic if reconnect fails
- @masterada Added multiline adapter
- @billimek sleeping and syncing to fix issues with docker hub builds

### Fixed
- @michaelshobbs fix working_directory so we don't duplicate test runs

Expand Down Expand Up @@ -178,7 +182,8 @@ All notable changes to this project will be documented in this file.
- Base container is now Alpine
- Moved to gliderlabs organization

[unreleased]: https://github.com/gliderlabs/logspout/compare/v3.2.4...HEAD
[unreleased]: https://github.com/gliderlabs/logspout/compare/v3.2.5...HEAD
[v3.2.5]: https://github.com/gliderlabs/logspout/compare/v3.2.4...v3.2.5
[v3.2.4]: https://github.com/gliderlabs/logspout/compare/v3.2.3...v3.2.4
[v3.2.3]: https://github.com/gliderlabs/logspout/compare/v3.2.2...v3.2.3
[v3.2.2]: https://github.com/gliderlabs/logspout/compare/v3.2.1...v3.2.2
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ RUN cd /src && ./build.sh "$(cat VERSION)"

ONBUILD COPY ./build.sh /src/build.sh
ONBUILD COPY ./modules.go /src/modules.go
ONBUILD RUN cd /src && chmod +x ./build.sh && ./build.sh "$(cat VERSION)-custom"
ONBUILD RUN cd /src && chmod +x ./build.sh && sleep 1 && sync && ./build.sh "$(cat VERSION)-custom"
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,32 @@ See [routesapi module](http://github.com/gliderlabs/logspout/blob/master/routesa

Logspout relies on the Docker API to retrieve container logs. A failure in the API may cause a log stream to hang. Logspout can detect and restart inactive Docker log streams. Use the environment variable `INACTIVITY_TIMEOUT` to enable this feature. E.g.: `INACTIVITY_TIMEOUT=1m` for a 1-minute threshold.

#### Multiline logging

In order to enable multiline logging, you must first prefix your adapter with the multiline adapter:

$ docker run \
--volume=/var/run/docker.sock:/var/run/docker.sock \
gliderlabs/logspout \
multiline+raw://192.168.10.10:5000?filter.name=*_db

Using the the above prefix enables multiline logging on all containers by default. To enable it only to specific containers set MULTILINE_ENABLE_DEFAULT=false for logspout, and use the LOGSPOUT_MULTILINE environment variable on the monitored container:

$ docker run -d -e 'LOGSPOUT_MULTILINE=true' image

##### MULTILINE_MATCH

Using the environment variable `MULTILINE_MATCH`=<first|last|nonfirst|nonlast> (default `nonfirst`) you define, which lines should be matched to the `MULTILINE_PATTERN`.
* first: match first line only and append following messages until you match another line
* last: concatenate all messages until the pattern matches the next line
* nonlast: match a line, append upcoming matching lines, also append first non-matching line and start
* nonfirst: append all matching lines to first line and start over with the next non-matching line

##### Important!
If you use multiline logging with raw, it's recommended to json encode the Data to avoid linebreaks in the output, eg:

"RAW_FORMAT={{ toJSON .Data }}\n"

#### Environment variables

* `ALLOW_TTY` - include logs from containers started with `-t` or `--tty` (i.e. `Allocate a pseudo-TTY`)
Expand All @@ -155,6 +181,11 @@ Logspout relies on the Docker API to retrieve container logs. A failure in the A
* `SYSLOG_STRUCTURED_DATA` - datum for structured data field
* `SYSLOG_TAG` - datum for tag field (default `{{.ContainerName}}+route.Options["append_tag"]`)
* `SYSLOG_TIMESTAMP` - datum for timestamp field (default `{{.Timestamp}}`)
* `MULTILINE_ENABLE_DEFAULT` - enable multiline logging for all containers when using the multiline adapter (default `true`)
* `MULTILINE_MATCH` - determines which lines the pattern should match, one of first|last|nonfirst|nonlast, for details see: [MULTILINE_MATCH](#multiline_match) (default `nonfirst`)
* `MULTILINE_PATTERN` - pattern for multiline logging, see: [MULTILINE_MATCH](#multiline_match) (default: `^\s`)
* `MULTILINE_FLUSH_AFTER` - maximum time between the first and last lines of a multiline log entry in milliseconds (default: 500)
* `MULTILINE_SEPARATOR` - separator between lines for output (default: `\n`)

#### Raw Format

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v3.2.4
v3.2.5
247 changes: 247 additions & 0 deletions adapters/multiline/multiline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package multiline

import (
"errors"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"

"github.com/fsouza/go-dockerclient"
"github.com/gliderlabs/logspout/router"
)

const (
matchFirst = "first"
matchLast = "last"
matchNonFirst = "nonfirst"
matchNonLast = "nonlast"
)

func init() {
router.AdapterFactories.Register(NewMultilineAdapter, "multiline")
}

// Adapter collects multi-lint log entries and sends them to the next adapter as a single entry
type Adapter struct {
out chan *router.Message
subAdapter router.LogAdapter
enableByDefault bool
pattern *regexp.Regexp
separator string
matchFirstLine bool
negateMatch bool
flushAfter time.Duration
checkInterval time.Duration
buffers map[string]*router.Message
nextCheck <-chan time.Time
}

// NewMultilineAdapter returns a configured multiline.Adapter
func NewMultilineAdapter(route *router.Route) (a router.LogAdapter, err error) {
enableByDefault := true
enableStr := os.Getenv("MULTILINE_ENABLE_DEFAULT")
if enableStr != "" {
var err error
enableByDefault, err = strconv.ParseBool(enableStr)
if err != nil {
return nil, errors.New("multiline: invalid value for MULTILINE_ENABLE_DEFAULT (must be true|false): " + enableStr)
}
}

pattern := os.Getenv("MULTILINE_PATTERN")
if pattern == "" {
pattern = `^\s`
}

separator := os.Getenv("MULTILINE_SEPARATOR")
if separator == "" {
separator = "\n"
}
patternRegexp, err := regexp.Compile(pattern)
if err != nil {
return nil, errors.New("multiline: invalid value for MULTILINE_PATTERN (must be regexp): " + pattern)
}

matchType := os.Getenv("MULTILINE_MATCH")
if matchType == "" {
matchType = matchNonFirst
}
matchType = strings.ToLower(matchType)
matchFirstLine := false
negateMatch := false
switch matchType {
case matchFirst:
matchFirstLine = true
negateMatch = false
case matchLast:
matchFirstLine = false
negateMatch = false
case matchNonFirst:
matchFirstLine = true
negateMatch = true
case matchNonLast:
matchFirstLine = false
negateMatch = true
default:
return nil, errors.New("multiline: invalid value for MULTILINE_MATCH (must be one of first|last|nonfirst|nonlast): " + matchType)
}

flushAfter := 500 * time.Millisecond
flushAfterStr := os.Getenv("MULTILINE_FLUSH_AFTER")
if flushAfterStr != "" {
timeoutMS, err := strconv.Atoi(flushAfterStr)
if err != nil {
return nil, errors.New("multiline: invalid value for multiline_timeout (must be number): " + flushAfterStr)
}
flushAfter = time.Duration(timeoutMS) * time.Millisecond
}

parts := strings.SplitN(route.Adapter, "+", 2)
if len(parts) != 2 {
return nil, errors.New("multiline: adapter must have a sub-adapter, eg: multiline+raw+tcp")
}

originalAdapter := route.Adapter
route.Adapter = parts[1]
factory, found := router.AdapterFactories.Lookup(route.AdapterType())
if !found {
return nil, errors.New("bad adapter: " + originalAdapter)
}
subAdapter, err := factory(route)
if err != nil {
return nil, err
}
route.Adapter = originalAdapter

out := make(chan *router.Message)
checkInterval := flushAfter / 2

return &Adapter{
out: out,
subAdapter: subAdapter,
enableByDefault: enableByDefault,
pattern: patternRegexp,
separator: separator,
matchFirstLine: matchFirstLine,
negateMatch: negateMatch,
flushAfter: flushAfter,
checkInterval: checkInterval,
buffers: make(map[string]*router.Message),
nextCheck: time.After(checkInterval),
}, nil
}

// Stream sends log data to the next adapter
func (a *Adapter) Stream(logstream chan *router.Message) {
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
a.subAdapter.Stream(a.out)
wg.Done()
}()
defer func() {
for _, message := range a.buffers {
a.out <- message
}

close(a.out)
wg.Wait()
}()

for {
select {
case message, ok := <-logstream:
if !ok {
return
}

if !multilineContainer(message.Container, a.enableByDefault) {
a.out <- message
continue
}

cID := message.Container.ID
old, oldExists := a.buffers[cID]
if a.isFirstLine(message) {
if oldExists {
a.out <- old
}

a.buffers[cID] = message
} else {
isLastLine := a.isLastLine(message)

if oldExists {
old.Data += a.separator + message.Data
message = old
}

if isLastLine {
a.out <- message
if oldExists {
delete(a.buffers, cID)
}
} else {
a.buffers[cID] = message
}
}
case <-a.nextCheck:
now := time.Now()

for key, message := range a.buffers {
if message.Time.Add(a.flushAfter).After(now) {
a.out <- message
delete(a.buffers, key)
}
}

a.nextCheck = time.After(a.checkInterval)
}
}
}

func (a *Adapter) isFirstLine(message *router.Message) bool {
if !a.matchFirstLine {
return false
}

match := a.pattern.MatchString(message.Data)
if a.negateMatch {
return !match
}

return match
}

func (a *Adapter) isLastLine(message *router.Message) bool {
if a.matchFirstLine {
return false
}

match := a.pattern.MatchString(message.Data)
if a.negateMatch {
return !match
}

return match
}

func multilineContainer(container *docker.Container, def bool) bool {
for _, kv := range container.Config.Env {
kvp := strings.SplitN(kv, "=", 2)
if len(kvp) == 2 && kvp[0] == "LOGSPOUT_MULTILINE" {
switch strings.ToLower(kvp[1]) {
case "true":
return true
case "false":
return false
}
return def
}
}

return def
}
Loading

0 comments on commit c9a891c

Please sign in to comment.