├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.custom ├── Dockerfile.dev ├── LICENSE ├── MODULES.md ├── Makefile ├── README.md ├── SPONSORS ├── VERSION ├── adapters ├── multiline │ ├── multiline.go │ └── multiline_test.go ├── raw │ └── raw.go └── syslog │ ├── syslog.go │ └── syslog_test.go ├── build-custom.sh ├── build.sh ├── cfg └── cfg.go ├── custom ├── Dockerfile ├── README.md ├── build.sh └── modules.go ├── debug ├── Dockerfile └── Makefile ├── go.mod ├── go.sum ├── healthcheck └── healthcheck.go ├── httpstream ├── README.md └── httpstream.go ├── logspout.go ├── modules.go ├── router ├── extpoints.go ├── http.go ├── persist.go ├── pump.go ├── pump_test.go ├── routes.go ├── routes_test.go └── types.go ├── routesapi ├── README.md └── routesapi.go ├── run-custom.sh └── transports ├── tcp └── tcp.go ├── tls ├── testdata │ ├── ca_int.pem │ ├── ca_root.pem │ ├── client_logspoutClient-key.pem │ ├── client_logspoutClient.pem │ ├── server_loggingEndpoint-key.pem │ └── server_loggingEndpoint.pem ├── tls.go └── tls_test.go └── udp └── udp.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | machine: true 5 | working_directory: /home/circleci/logspout 6 | environment: 7 | DEBUG: "true" 8 | steps: 9 | - checkout 10 | - run: | 11 | make circleci 12 | - run: | 13 | make lint-ci 14 | - run: | 15 | make build 16 | - run: | 17 | make build-custom 18 | - run: | 19 | make -e test-image-size 20 | - run: | 21 | make -e test 22 | - run: | 23 | make -e test-tls 24 | - run: | 25 | make -e test-healthcheck 26 | - run: | 27 | make -e test-custom 28 | - run: | 29 | make -e test-tls-custom 30 | - store_artifacts: 31 | path: build 32 | destination: build 33 | - deploy: 34 | name: release 35 | command: | 36 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 37 | make release 38 | fi 39 | publish: 40 | machine: 41 | image: ubuntu-1604:202007-01 42 | working_directory: /home/circleci/logspout 43 | environment: 44 | DEBUG: "true" 45 | steps: 46 | - checkout 47 | - run: make publish-requirements 48 | - run: make publish-test 49 | - run: | 50 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 51 | docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 52 | make publish-master 53 | fi 54 | - run: | 55 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 56 | docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 57 | make publish-release 58 | fi 59 | 60 | workflows: 61 | version: 2 62 | build_and_publish: 63 | jobs: 64 | - build 65 | - publish: 66 | requires: 67 | - build 68 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | release 2 | build 3 | .git 4 | vendor 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /logspout 2 | release 3 | build 4 | vendor/ 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | # ignore cases where we truly don't care about the returned error 4 | ignore: net/http:^Write$,io:^WriteString$ 5 | govet: 6 | check-shadowing: true 7 | settings: 8 | printf: 9 | funcs: 10 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 11 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 12 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 13 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 14 | golint: 15 | min-confidence: 0 16 | gocyclo: 17 | min-complexity: 12 18 | maligned: 19 | suggest-new: true 20 | dupl: 21 | threshold: 100 22 | goconst: 23 | min-len: 2 24 | min-occurrences: 2 25 | depguard: 26 | list-type: blacklist 27 | misspell: 28 | locale: US 29 | lll: 30 | line-length: 160 31 | goimports: 32 | local-prefixes: github.com/gliderlabs/logspout 33 | gocritic: 34 | settings: 35 | hugeParam: 36 | sizeThreshold: 160 37 | enabled-tags: 38 | - performance 39 | nakedret: 40 | max-func-lines: 65 41 | 42 | linters: 43 | enable-all: true 44 | disable: 45 | - maligned 46 | - prealloc 47 | - gochecknoglobals 48 | - funlen 49 | - gochecknoinits 50 | - godot 51 | - wsl 52 | - nolintlint 53 | - testpackage 54 | - dupl 55 | - goerr113 56 | 57 | issues: 58 | # Excluding configuration per-path, per-linter, per-text and per-source 59 | exclude-rules: 60 | # Exclude some linters from running on tests files. 61 | - path: _test\.go 62 | linters: 63 | - dupl 64 | - errcheck 65 | - goconst 66 | - gocyclo 67 | - gosec 68 | - lll 69 | - nakedret 70 | - unparam 71 | - funlen 72 | 73 | run: 74 | deadline: 2m 75 | issues-exit-code: 1 76 | 77 | # golangci.com configuration 78 | # https://github.com/golangci/golangci/wiki/Configuration 79 | service: 80 | golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly 81 | prepare: 82 | - echo "here I can run custom commands, but no preparation needed for this repo" 83 | 84 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [Unreleased][unreleased] 5 | ### Fixed 6 | 7 | ### Added 8 | 9 | ### Removed 10 | 11 | ### Changed 12 | 13 | ## [v3.2.14] - 2021-12-03 14 | ### Fixed 15 | - @0xflotus fix: typo error in project name 16 | 17 | ### Changed 18 | - @skyzh upgrade dockerclient dependency 19 | - @merowing1279 retrieve logs from already started containers 20 | - @odidev Release docker image for arm64 21 | 22 | ## [v3.2.13] - 2020-11-26 23 | ### Changed 24 | - @michaelshobbs bump golangci-lint to 1.27 and fix lintballs 25 | 26 | ### Fixed 27 | - @michaelshobbs fix backlog() logic and add tests 28 | 29 | ## [v3.2.12] - 2020-10-22 30 | ### Changed 31 | - @michaelshobbs bump alpine to 3.12 32 | 33 | ## [v3.2.11] - 2020-05-08 34 | ### Added 35 | - @hhromic Add Syslog TCP framing documentation to README 36 | 37 | ### Changed 38 | - @hhromic syslog adapter refactor 39 | - @michaelshobbs use type assertion instead of reflection to determine connection type 40 | - @michaelshobbs use // + space for all human readable comments 41 | 42 | ## [v3.2.10] - 2020-05-01 43 | ### Added 44 | - @jszwedko Add optional TCP framing to syslog adapter 45 | 46 | ### Fixed 47 | - @bbigras add missing syntax highlighting in README.md 48 | 49 | ## [v3.2.9] - 2020-04-30 50 | ### Fixed 51 | - @bbigras add missing syntax highlighting in README.md 52 | 53 | ### Added 54 | - @edorgeville Adds `db` log driver to `logDriverSupported` 55 | - @renehernandez Add support for multiple exclusion labels 56 | - @renehernandez Add support for EXCLUDE_LABELS envvar with fallback to existing EXCLUDE_LABEL 57 | - @hhromic adapters/syslog: add ContainerNameSplitN utility message function 58 | 59 | ### Changed 60 | - @hhromic adapters/syslog: enforce RFC size limits in message fields 61 | 62 | ## [v3.2.8] - 2020-04-03 63 | ### Changed 64 | - @michaelshobbs bump alpine to 3.11 and go to 1.13.4-r1 65 | 66 | ## [v3.2.7] - 2020-04-03 67 | ### Fixed 68 | - @CodeLingoBot @gbolo Fix function comments based on best practices from Effective Go 69 | 70 | ### Changed 71 | - @michaelshobbs update alpine to 3.10/go 1.12.12-r0 and fix linting 72 | - @whoisteri DOC Document accessible data in RAW_FORMAT template 73 | - @tiagorlampert DOC typos 74 | - @michaelshobbs DOC CHANGLELOG formatting 75 | - @tomlankhorst DOC Suggest to disable userns-remap for logspout 76 | - @StudioEtrange DOC add link to logspout-fluentd 77 | 78 | ## [v3.2.6] - 2018-10-04 79 | ### Fixed 80 | - @jdgiotta Spelling corrections and fixed stack compose formatting in example 81 | - @dylanmei dylanmei Update 3rd party module link in README 82 | 83 | ### Added 84 | - @vbeausoleil added a simple healthcheck 85 | - @gbolo added option to load TLS client certificate and key 86 | - @gbolo added ability to control the TLS client trust store 87 | - @gbolo added option to harden the TLS client 88 | - @chopmann added option to bind the http server to an address 89 | - @ibrokethecloud added ability to add custom key:value pairs as EXCLUDE_LABEL 90 | 91 | ### Changed 92 | - @develar alpine 3.8 + golang 1.10.1 93 | - @gbolo enforced the use of `go 1.8+` in order to accommodate some TLS settings 94 | 95 | ## [v3.2.5] - 2018-06-05 96 | ### Fixed 97 | - @michaelshobbs fix working_directory so we don't duplicate test runs 98 | - @gmelika panic if reconnect fails 99 | - @masterada Added multiline adapter 100 | - @billimek sleeping and syncing to fix issues with docker hub builds 101 | 102 | ### Added 103 | - @chris7444 take the hostname from /etc/host_hostname if the file is there 104 | - @chris7444 update README.md for swarm deployments PR #329 105 | - @nvanheuverzwijn strip \r and \n when reading the file /etc/host_hostname 106 | - @lucassabreu toJSON with examples 107 | 108 | ### Changed 109 | - @michaelshobbs pass debug to test container 110 | - @jgreat Strip header bytes from log stream 111 | - @trondvh chmod +x build.sh 112 | - @develar alpine 3.7 + golang 1.9.2 113 | 114 | ## [v3.2.3] - 2017-09-23 115 | ### Added 116 | - @guigouz guigouz Add `RAW_FORMAT` to the documentation 117 | - @stevecalvert Allow docker log tail to be specified, default to 'all 118 | 119 | ### Fixed 120 | - @jeanlouisboudart RawTerminal should be set to true if we want to collect tty logs 121 | - @michaelshobbs fix new golint lintballs 122 | 123 | ## [v3.2.2] - 2017-05-25 124 | ### Fixed 125 | - @michaelshobbs router: fix empty routes response. fixes #299 126 | - @Crashthatch Close existing routes when adding a new route with an existing ID. fixes #305 127 | 128 | ### Changed 129 | - @mattaitchison router/pump: remove logstream send timeout 130 | 131 | ## [v3.2.1] - 2017-04-13 132 | ### Fixed 133 | - @michaelshobbs build: fix missing ca-certificates. closes #294 134 | 135 | ### Added 136 | - @michaelshobbs build: add tls test case 137 | 138 | ### Changed 139 | - @michaelshobbs use circleci 2.0 140 | 141 | ## [v3.2] - 2017-04-13 142 | ### Fixed 143 | - @ekkinox FIX: add build-base package install to fix missing gcc 144 | - @bobzoller reconnect log stream unless container is dead 145 | - @mattaitchison Fix locking around custom route loading 146 | - @bobzoller avoid duplicate pumps with mutex lock 147 | - @markine Use InactivityTimeout to work around a bug in docker (#204) 148 | - @mattaitchison install ca-certificates fixes #247 149 | - @mattaitchison Dockerfile: use alpine 3.5 to fix build issue from missing context pkg 150 | 151 | ### Added 152 | - @micahhausler Add Graylog GELF module 153 | - @Selim Ekizoglu Exclude containers by label. 154 | - @treeder Some help for working on custom modules 155 | - @davidnortonjr Add more configuration examples to README 156 | - @davidnortonjr Filter by label (#236) 157 | - @mattaitchison first pass at tests (#218) 158 | - @grosskur syslog: Add support for SYSLOG_TIMESTAMP (#260) 159 | - @michaelshobbs add linting with go vet and golint 160 | - @andrewgaul Allow configuration of retry count via environment 161 | - @davidnortonjr Allow containers with TTY enabled using environment variable ALLOW_TTY 162 | - @michaelshobbs add golint in ci and filter /vendor/ from linting 163 | - @ebr add env.var switch to turn off backlogs 164 | - @michaelshobbs add test for max image size 165 | 166 | ### Changed 167 | - @selimekizoglu Ignore empty EXCLUDE_LABEL values 168 | - @pmbauer ignore containers with unsupported log drivers 169 | - @robertjustjones Updated README to include tls 170 | - @jmreicha custom: Update README and include example build script 171 | - @josegonzalez Add a note about build.sh needing to be in the docker build directory 172 | - @treeder Much, much faster builds 173 | - @michaelshobbs set common test name prefix for -run ease 174 | - @michaelshobbs make ignoreContainerTTY more testable and add test 175 | - @michaelshobbs make retryCount testable and add test 176 | - @michaelshobbs use glide in dockerfile 177 | - @michaelshobbs use alpine + build script and add test for custom image building 178 | - @michaelshobbs attempt to preserve buffer on reconnect() 179 | - @michaelshobbs race detector for alpine is broken. disable it for now 180 | - @michaelshobbs make vet more reliable 181 | - @luketurner Don't retry sending on ECONNRESET 182 | 183 | ## [v3.1] - 2016-05-23 184 | ### Fixed 185 | - Panic when renaming stopped container #183 186 | - won't start without route configuration #185 187 | - RouteManager.Name() didn't return name 188 | ### Added 189 | - update container name if we get a rename event. closes #144 (#180) 190 | 191 | ### Removed 192 | 193 | ### Changed 194 | - Now using Alpine Linux 3.3 and GO 1.5.3, removed the "edge" package repo for building the official Docker image (#174) 195 | - Fix exposed ports in Dockerfile and readme. Remove references to /tmp/docker.sock from readme 196 | 197 | ## [v3] - 2016-03-03 198 | ### Fixed 199 | - use start/die like old version not create/destroy 200 | - performance fix, generalizing SyslogMessage, minor cleanups 201 | - Initialize Route options map 202 | - Fixed a couple of typos, updated narrative 203 | - UDP message delivery should not kill the program 204 | - Exit with return code 1 on job setup failure 205 | - Simplify and add early exit to RoutingFrom 206 | - Unmarshal without buffering 207 | - Remove unnecessary closure 208 | - Undo change introduced in 07555c5 209 | - Fix port number in httpstream example 210 | - Use correct nilvalue for structured data as per rfc 5424 211 | - retry tcp errors and don't hang forever on failure 212 | 213 | ### Added 214 | - mention irc channel 215 | - allowing easy custom builds of logspout 216 | - Allow env vars in stream URLs 217 | - Allow you to ignore log messages from individual containers by setting container environment variable, LOGSPOUT=ignore, when starting 218 | - Add URL for Logstash module 219 | - Adding CircleCI, Docker and IRC badges to readme. 220 | - Add TLS transport. Fixes #116 221 | 222 | ### Removed 223 | - Removed attach on restart event 224 | - remove dev containers 225 | - Removed deprecated library hosted in google code in favor of its new home 226 | 227 | ### Changed 228 | - switched to gliderlabs org 229 | - assume build 230 | - rough pass at breaking logspout.go into separate packages 231 | - fully split up packages. major refactoring of router 232 | - simpler matching. working routesapi. dropped old utils 233 | - make sure all uri params get into route options 234 | - readme updates and module specific readmes 235 | - renamed ConnectionFactory to AdapterTransport 236 | - updated readme to use current schema 237 | - names and parama 238 | - more readable 239 | - hold handler from returning until streamer finishes 240 | - primarily designed new boot output, but came with it architectural changes 241 | - updating docker sock location 242 | - support old location for docker socket 243 | - force link in case its run again, such as with custom builds 244 | - analytics test 245 | - update analytics 246 | - Update README.md 247 | - Update README with tls module 248 | - Wrong port in README.md #136 249 | 250 | 251 | ## [v2] - 2015-02-12 252 | ### Added 253 | - Allow comma-separated routes on boot 254 | - Added project versioning 255 | - Development Dockerfile and make task 256 | - Deis sponsorship / support 257 | 258 | ### Removed 259 | - Staging binary. Built entirely in Docker. 260 | - Dropped unnecessary layers in Dockerfile 261 | 262 | ### Changed 263 | - Base container is now Alpine 264 | - Moved to gliderlabs organization 265 | 266 | [unreleased]: https://github.com/gliderlabs/logspout/compare/v3.2.14...HEAD 267 | [v3.2.14]: https://github.com/gliderlabs/logspout/compare/v3.2.13...v3.2.14 268 | [v3.2.13]: https://github.com/gliderlabs/logspout/compare/v3.2.12...v3.2.13 269 | [v3.2.12]: https://github.com/gliderlabs/logspout/compare/v3.2.11...v3.2.12 270 | [v3.2.11]: https://github.com/gliderlabs/logspout/compare/v3.2.10...v3.2.11 271 | [v3.2.10]: https://github.com/gliderlabs/logspout/compare/v3.2.9...v3.2.10 272 | [v3.2.9]: https://github.com/gliderlabs/logspout/compare/v3.2.8...v3.2.9 273 | [v3.2.8]: https://github.com/gliderlabs/logspout/compare/v3.2.7...v3.2.8 274 | [v3.2.7]: https://github.com/gliderlabs/logspout/compare/v3.2.6...v3.2.7 275 | [v3.2.6]: https://github.com/gliderlabs/logspout/compare/v3.2.5...v3.2.6 276 | [v3.2.5]: https://github.com/gliderlabs/logspout/compare/v3.2.4...v3.2.5 277 | [v3.2.4]: https://github.com/gliderlabs/logspout/compare/v3.2.3...v3.2.4 278 | [v3.2.3]: https://github.com/gliderlabs/logspout/compare/v3.2.2...v3.2.3 279 | [v3.2.2]: https://github.com/gliderlabs/logspout/compare/v3.2.1...v3.2.2 280 | [v3.2.1]: https://github.com/gliderlabs/logspout/compare/v3.2...v3.2.1 281 | [v3.2]: https://github.com/gliderlabs/logspout/compare/v3.1...v3.2 282 | [v3.1]: https://github.com/gliderlabs/logspout/compare/v3...v3.1 283 | [v3]: https://github.com/gliderlabs/logspout/compare/v2...v3 284 | [v2]: https://github.com/gliderlabs/logspout/compare/v1...v2 285 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | ENTRYPOINT ["/bin/logspout"] 3 | VOLUME /mnt/routes 4 | EXPOSE 80 5 | 6 | COPY . /src 7 | RUN cd /src && ./build.sh "$(cat VERSION)" 8 | 9 | ONBUILD COPY ./build.sh /src/build.sh 10 | ONBUILD COPY ./modules.go /src/modules.go 11 | ONBUILD RUN cd /src && chmod +x ./build.sh && sleep 1 && sync && ./build.sh "$(cat VERSION)-custom" 12 | -------------------------------------------------------------------------------- /Dockerfile.custom: -------------------------------------------------------------------------------- 1 | FROM iron/go:dev 2 | 3 | WORKDIR /app 4 | ADD . /app 5 | 6 | ENTRYPOINT ["./logspout"] 7 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | VOLUME /mnt/routes 3 | EXPOSE 80 4 | 5 | RUN apk --no-cache add go build-base git mercurial ca-certificates curl 6 | COPY . /src 7 | WORKDIR /src 8 | CMD go build -ldflags "-X main.Version=dev" -o /bin/logspout \ 9 | && exec /bin/logspout 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Glider Labs, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MODULES.md: -------------------------------------------------------------------------------- 1 | Instructions on how to build/test your own modules. 2 | 3 | ## Getting Started 4 | 5 | 1. Fork this repository 6 | 1. Create a new repository for your adapter 7 | 1. Copy something like [raw.go](https://github.com/gliderlabs/logspout/blob/master/adapters/raw/raw.go) to get started. 8 | 1. Add your module to modules.go 9 | 10 | > You'll need to add the `build.sh` from this repository to the directory from which you run `docker build` or you will get errors 11 | 12 | Now build and run logspout with your adapter, replace SYSLOG with your own syslog url. 13 | 14 | ```sh 15 | SYSLOG=syslog://logs.papertrailapp.com:55555 ./run-custom.sh 16 | ``` 17 | 18 | Now let's add your new adapter to the running logspout (replace address below with your final stats destination): 19 | 20 | ```sh 21 | curl http://localhost:8000/routes -d '{ 22 | "adapter": "myadapter", 23 | "filter_sources": ["stdout" ,"stderr"], 24 | "address": "localhost:1234" 25 | }' 26 | ``` 27 | 28 | Now any log messages that come out of any container on your machine will go through your adapter. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=logspout 2 | VERSION=$(shell cat VERSION) 3 | # max image size of 40MB 4 | MAX_IMAGE_SIZE := 40000000 5 | 6 | GOBIN := $(shell go env GOPATH | awk -F ":" '{ print $$1 }')/bin 7 | GOLANGCI_LINT_VERSION := v1.27.0 8 | 9 | ifeq ($(shell uname), Darwin) 10 | XARGS_ARG="-L1" 11 | endif 12 | GOPACKAGES ?= $(shell go list ./... | egrep -v 'custom|vendor') 13 | TEST_ARGS ?= -race 14 | 15 | ifdef TEST_RUN 16 | TESTRUN := -run ${TEST_RUN} 17 | endif 18 | 19 | build-dev: 20 | docker build -f Dockerfile.dev -t $(NAME):dev . 21 | 22 | dev: build-dev 23 | @docker run --rm \ 24 | -e DEBUG=true \ 25 | -v /var/run/docker.sock:/var/run/docker.sock \ 26 | -v $(PWD):/go/src/github.com/gliderlabs/logspout \ 27 | -p 8000:80 \ 28 | -e ROUTE_URIS=$(ROUTE) \ 29 | $(NAME):dev 30 | 31 | build: 32 | mkdir -p build 33 | docker build -t $(NAME):$(VERSION) . 34 | docker save $(NAME):$(VERSION) | gzip -9 > build/$(NAME)_$(VERSION).tgz 35 | 36 | build-custom: 37 | docker tag $(NAME):$(VERSION) gliderlabs/$(NAME):master 38 | cd custom && docker build -t $(NAME):custom . 39 | 40 | lint-requirements: 41 | ifeq ($(shell which golangci-lint), ) 42 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOBIN) $(GOLANGCI_LINT_VERSION) 43 | endif 44 | 45 | lint: lint-requirements 46 | $(GOBIN)/golangci-lint run 47 | 48 | lint-ci-direct: lint-requirements 49 | $(GOBIN)/golangci-lint --verbose run 50 | 51 | lint-ci: build-dev 52 | docker run \ 53 | -v $(PWD):/go/src/github.com/gliderlabs/logspout \ 54 | $(NAME):dev make -e lint-ci-direct 55 | 56 | test: build-dev 57 | docker run \ 58 | -v /var/run/docker.sock:/var/run/docker.sock \ 59 | -v $(PWD):/go/src/github.com/gliderlabs/logspout \ 60 | -e TEST_ARGS="" \ 61 | -e DEBUG=$(DEBUG) \ 62 | $(NAME):dev make -e test-direct 63 | 64 | test-direct: 65 | go test -p 1 -v $(TEST_ARGS) $(GOPACKAGES) $(TESTRUN) 66 | 67 | test-image-size: 68 | @if [ $(shell docker inspect -f '{{ .Size }}' $(NAME):$(VERSION)) -gt $(MAX_IMAGE_SIZE) ]; then \ 69 | echo ERROR: image size greater than $(MAX_IMAGE_SIZE); \ 70 | exit 2; \ 71 | fi 72 | 73 | test-tls: 74 | docker run -d --name $(NAME)-tls \ 75 | -v /var/run/docker.sock:/var/run/docker.sock \ 76 | $(NAME):$(VERSION) syslog+tls://logs3.papertrailapp.com:54202 77 | sleep 2 78 | docker logs $(NAME)-tls 79 | docker inspect --format='{{ .State.Running }}' $(NAME)-tls | grep true 80 | docker stop $(NAME)-tls || true 81 | docker rm $(NAME)-tls || true 82 | 83 | test-healthcheck: 84 | docker run -d --name $(NAME)-healthcheck \ 85 | -p 8000:80 \ 86 | -v /var/run/docker.sock:/var/run/docker.sock \ 87 | $(NAME):$(VERSION) 88 | sleep 5 89 | docker logs $(NAME)-healthcheck 90 | docker inspect --format='{{ .State.Running }}' $(NAME)-healthcheck | grep true 91 | curl --head --silent localhost:8000/health | grep "200 OK" 92 | docker stop $(NAME)-healthcheck || true 93 | docker rm $(NAME)-healthcheck || true 94 | 95 | test-custom: 96 | docker run --name $(NAME)-custom $(NAME):custom || true 97 | docker logs $(NAME)-custom 2>&1 | grep -q logstash 98 | docker rmi gliderlabs/$(NAME):master || true 99 | docker rm $(NAME)-custom || true 100 | 101 | test-tls-custom: 102 | docker run -d --name $(NAME)-tls-custom \ 103 | -v /var/run/docker.sock:/var/run/docker.sock \ 104 | $(NAME):custom syslog+tls://logs3.papertrailapp.com:54202 105 | sleep 2 106 | docker logs $(NAME)-tls-custom 107 | docker inspect --format='{{ .State.Running }}' $(NAME)-tls-custom | grep true 108 | docker stop $(NAME)-tls-custom || true 109 | docker rm $(NAME)-tls-custom || true 110 | 111 | .PHONY: release 112 | release: 113 | rm -rf release && mkdir release 114 | go get github.com/progrium/gh-release/... 115 | cp build/* release 116 | gh-release create gliderlabs/$(NAME) $(VERSION) \ 117 | $(shell git rev-parse --abbrev-ref HEAD) $(VERSION) 118 | 119 | .PHONY: circleci 120 | circleci: 121 | ifneq ($(CIRCLE_BRANCH), release) 122 | echo build-$$CIRCLE_BUILD_NUM > VERSION 123 | endif 124 | 125 | .PHONY: clean 126 | clean: 127 | rm -rf build/ 128 | docker rm $(shell docker ps -aq) || true 129 | docker rmi $(NAME):dev $(NAME):$(VERSION) || true 130 | docker rmi $(shell docker images -f 'dangling=true' -q) || true 131 | 132 | .PHONY: publish-requirements 133 | publish-requirements: 134 | mkdir -vp ~/.docker/cli-plugins/ 135 | curl --silent -L --output ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.3.1/buildx-v0.3.1.linux-amd64 136 | chmod a+x ~/.docker/cli-plugins/docker-buildx 137 | docker run -it --rm --privileged tonistiigi/binfmt --install all 138 | docker buildx create --use --name mybuilder 139 | 140 | .PHONY: publish-test 141 | publish-test: 142 | docker buildx build --load --platform linux/amd64 -t gliderlabs/$(NAME):linux-amd64-${CIRCLE_BRANCH} . 143 | docker buildx build --load --platform linux/arm/v6 -t gliderlabs/$(NAME):linux-arm-v6-${CIRCLE_BRANCH} . 144 | docker buildx build --load --platform linux/arm64 -t gliderlabs/$(NAME):linux-arm64-${CIRCLE_BRANCH} . 145 | docker images 146 | 147 | .PHONY: publish-master 148 | publish-master: 149 | docker buildx build --push --platform linux/amd64,linux/arm/v6,linux/arm64 -t gliderlabs/$(NAME):master -t gliderlabs/$(NAME):latest . 150 | 151 | .PHONY: publish-release 152 | publish-release: 153 | docker buildx build --push --platform linux/amd64,linux/arm/v6,linux/arm64 -t gliderlabs/$(NAME):$(VERSION) . 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logspout 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/gliderlabs/logspout/release.svg)](https://circleci.com/gh/gliderlabs/logspout) 4 | [![Docker pulls](https://img.shields.io/docker/pulls/gliderlabs/logspout.svg)](https://hub.docker.com/r/gliderlabs/logspout/) 5 | [![IRC Channel](https://img.shields.io/badge/irc-%23gliderlabs-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#gliderlabs) 6 | 7 | > Docker Hub automated builds for `gliderlabs/logspout:latest` and `progrium/logspout:latest` are now pointing to the `release` branch. For `master`, use `gliderlabs/logspout:master`. Individual versions are also available as saved images in [releases](https://github.com/gliderlabs/logspout/releases). 8 | 9 | Logspout is a log router for Docker containers that runs inside Docker. It attaches to all containers on a host, then routes their logs wherever you want. It also has an extensible module system. 10 | 11 | It's a mostly stateless log appliance. It's not meant for managing log files or looking at history. It is just a means to get your logs out to live somewhere else, where they belong. 12 | 13 | For now it only captures stdout and stderr, but a module to collect container syslog is planned. 14 | 15 | ## Getting logspout 16 | 17 | Logspout is a very small Docker container (15.2MB virtual, based on [Alpine](https://github.com/gliderlabs/docker-alpine)). Pull the latest release from the index: 18 | 19 | $ docker pull gliderlabs/logspout:latest 20 | 21 | You can also download and load a specific version: 22 | 23 | $ curl -s dl.gliderlabs.com/logspout/v2.tgz | docker load 24 | 25 | ## Using logspout 26 | 27 | #### Route all container output to remote syslog 28 | 29 | The simplest way to use logspout is to just take all logs and ship to a remote syslog. Just pass a syslog URI (or several comma separated URIs) as the command. Here we show use of the `tls` encrypted transport option in the URI. Also, we always mount the Docker Unix socket with `-v` to `/var/run/docker.sock`: 30 | 31 | $ docker run --name="logspout" \ 32 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 33 | gliderlabs/logspout \ 34 | syslog+tls://logs.papertrailapp.com:55555 35 | 36 | logspout will gather logs from other containers that are started **without the `-t` option** and are configured with a logging driver that works with `docker logs` (`journald` and `json-file`). 37 | 38 | To see what data is used for syslog messages, see the [syslog adapter](http://github.com/gliderlabs/logspout/blob/master/adapters) docs. 39 | 40 | The container must be able to access the Docker Unix socket to mount it. This is typically a problem when [namespace remapping](https://docs.docker.com/engine/security/userns-remap/) is enabled. To disable remapping for the logspout container, pass the `--userns=host` flag to `docker run`, `.. create`, etc. 41 | 42 | #### Ignoring specific containers 43 | 44 | You can tell logspout to ignore specific containers by setting an environment variable when starting your container, like so:- 45 | 46 | $ docker run -d -e 'LOGSPOUT=ignore' image 47 | 48 | Or, by adding a label which you define by setting an environment variable when running logspout: 49 | 50 | $ docker run --name="logspout" \ 51 | -e EXCLUDE_LABEL=logspout.exclude \ 52 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 53 | gliderlabs/logspout 54 | $ docker run -d --label logspout.exclude=true image 55 | 56 | Logspout also allows to ignore containers by specifying a list of labels using the environment variables `EXCLUDE_LABELS` or `EXCLUDE_LABEL`, using the `;` as separator: 57 | 58 | $ $ docker run --name="logspout" \ 59 | -e EXCLUDE_LABELS=k8s:app;backend:rails;io.kubernetes.pod.namespace:default \ 60 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 61 | gliderlabs/logspout 62 | $ docker run -d --label k8s=app image1 63 | $ docker run -d --label backend=rails image2 64 | 65 | **NOTE** Setting `EXCLUDE_LABELS` would take precedence over setting `EXCLUDE_LABEL` 66 | 67 | #### Including specific containers 68 | 69 | You can tell logspout to only include certain containers by setting filter parameters on the URI: 70 | 71 | $ docker run \ 72 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 73 | gliderlabs/logspout \ 74 | raw://192.168.10.10:5000?filter.name=*_db 75 | 76 | $ docker run \ 77 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 78 | gliderlabs/logspout \ 79 | raw://192.168.10.10:5000?filter.id=3b6ba57db54a 80 | 81 | $ docker run \ 82 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 83 | gliderlabs/logspout \ 84 | raw://192.168.10.10:5000?filter.sources=stdout%2Cstderr 85 | 86 | # Forward logs from containers with both label 'a' starting with 'x', and label 'b' ending in 'y'. 87 | $ docker run \ 88 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 89 | gliderlabs/logspout \ 90 | raw://192.168.10.10:5000?filter.labels=a:x*%2Cb:*y 91 | 92 | Note that you must URL-encode parameter values such as the comma in `filter.sources` and `filter.labels`. 93 | 94 | #### Multiple logging destinations 95 | 96 | You can route to multiple destinations by comma-separating the URIs: 97 | 98 | $ docker run \ 99 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 100 | gliderlabs/logspout \ 101 | raw://192.168.10.10:5000?filter.name=*_db,syslog+tls://logs.papertrailapp.com:55555?filter.name=*_app 102 | 103 | #### Suppressing backlog tail 104 | You can tell logspout to only display log entries since container "start" or "restart" event by setting a `BACKLOG=false` environment variable (equivalent to `docker logs --since=0s`): 105 | 106 | $ docker run -d --name="logspout" \ 107 | -e 'BACKLOG=false' \ 108 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 109 | gliderlabs/logspout 110 | 111 | The default behaviour is to output all logs since creation of the container (equivalent to `docker logs --tail=all` or simply `docker logs`). 112 | 113 | > NOTE: Use of this option **may** cause the first few lines of log output to be missed following a container being started, if the container starts outputting logs before logspout has a chance to see them. If consistent capture of *every* line of logs is critical to your application, you might want to test thoroughly and/or avoid this option (at the expense of getting the entire backlog for every restarting container). This does not affect containers that are removed and recreated. 114 | 115 | 116 | #### Environment variable, TAIL 117 | Whilst BACKLOG=false restricts the tail by setting the Docker Logs.Options.Since to time.Now(), another mechanism to restrict the tail is to set TAIL=n. Use of this mechanism avoids parsing the earlier content of the logfile which may have a speed advantage if the tail content is of no interest or has become corrupted. 118 | 119 | #### Inspect log streams using curl 120 | 121 | Using the [httpstream module](http://github.com/gliderlabs/logspout/blob/master/httpstream), you can connect with curl to see your local aggregated logs in realtime. You can do this without setting up a route URI. 122 | 123 | $ docker run -d --name="logspout" \ 124 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 125 | --publish=127.0.0.1:8000:80 \ 126 | gliderlabs/logspout 127 | $ curl http://127.0.0.1:8000/logs 128 | 129 | You should see a nicely colored stream of all your container logs. You can filter by container name and more. You can also get JSON objects, or you can upgrade to WebSocket and get JSON logs in your browser. 130 | 131 | See [httpstream module](http://github.com/gliderlabs/logspout/blob/master/httpstream) for all options. 132 | 133 | #### Create custom routes via HTTP 134 | 135 | Using the [routesapi module](http://github.com/gliderlabs/logspout/blob/master/routesapi) logspout can also expose a `/routes` resource to create and manage routes. 136 | 137 | $ curl $(docker port `docker ps -lq` 8000)/routes \ 138 | -X POST \ 139 | -d '{"source": {"filter": "db", "types": ["stderr"]}, "target": {"type": "syslog", "addr": "logs.papertrailapp.com:55555"}}' 140 | 141 | That example creates a new syslog route to [Papertrail](https://papertrailapp.com) of only `stderr` for containers with `db` in their name. 142 | 143 | Routes are stored on disk, so by default routes are ephemeral. You can mount a volume to `/mnt/routes` to persist them. 144 | 145 | See [routesapi module](http://github.com/gliderlabs/logspout/blob/master/routesapi) for all options. 146 | 147 | #### Detecting timeouts in Docker log streams 148 | 149 | 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. 150 | 151 | #### Multiline logging 152 | 153 | In order to enable multiline logging, you must first prefix your adapter with the multiline adapter: 154 | 155 | $ docker run \ 156 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 157 | gliderlabs/logspout \ 158 | multiline+raw://192.168.10.10:5000?filter.name=*_db 159 | 160 | 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: 161 | 162 | $ docker run -d -e 'LOGSPOUT_MULTILINE=true' image 163 | 164 | ##### MULTILINE_MATCH 165 | 166 | Using the environment variable `MULTILINE_MATCH`= (default `nonfirst`) you define, which lines should be matched to the `MULTILINE_PATTERN`. 167 | * first: match first line only and append following messages until you match another line 168 | * last: concatenate all messages until the pattern matches the next line 169 | * nonlast: match a line, append upcoming matching lines, also append first non-matching line and start 170 | * nonfirst: append all matching lines to first line and start over with the next non-matching line 171 | 172 | ##### Important! 173 | If you use multiline logging with raw, it's recommended to json encode the Data to avoid line breaks in the output, eg: 174 | 175 | "RAW_FORMAT={{ toJSON .Data }}\n" 176 | 177 | #### Environment variables 178 | 179 | * `ALLOW_TTY` - include logs from containers started with `-t` or `--tty` (i.e. `Allocate a pseudo-TTY`) 180 | * `BACKLOG` - suppress container tail backlog 181 | * `TAIL` - specify the number of lines in the log tail to capture when logspout starts (default `all`) 182 | * `DEBUG` - emit debug logs 183 | * `EXCLUDE_LABEL` - exclude containers with a given label. The label can have a value of true or a custom value matched with : after the label name like label_name:label_value. 184 | * `INACTIVITY_TIMEOUT` - detect hang in Docker API (default 0) 185 | * `HTTP_BIND_ADDRESS` - configure which interface address to listen on (default 0.0.0.0) 186 | * `PORT` or `HTTP_PORT` - configure which port to listen on (default 80) 187 | * `RAW_FORMAT` - log format for the raw adapter (default `{{.Data}}\n`) 188 | * `RETRY_COUNT` - how many times to retry a broken socket (default 10) 189 | * `ROUTESPATH` - path to routes (default `/mnt/routes`) 190 | * `SYSLOG_DATA` - datum for data field (default `{{.Data}}`) 191 | * `SYSLOG_FORMAT` - syslog format to emit, either `rfc3164` or `rfc5424` (default `rfc5424`) 192 | * `SYSLOG_HOSTNAME` - datum for hostname field (default `{{.Container.Config.Hostname}}`) 193 | * `SYSLOG_PID` - datum for pid field (default `{{.Container.State.Pid}}`) 194 | * `SYSLOG_PRIORITY` - datum for priority field (default `{{.Priority}}`) 195 | * `SYSLOG_STRUCTURED_DATA` - datum for structured data field 196 | * `SYSLOG_TAG` - datum for tag field (default `{{.ContainerName}}+route.Options["append_tag"]`) 197 | * `SYSLOG_TCP_FRAMING` - for TCP or TLS transports, whether to use `octet-counted` framing in emitted messages or `traditional` LF framing (default `traditional`) 198 | * `SYSLOG_TIMESTAMP` - datum for timestamp field (default `{{.Timestamp}}`) 199 | * `MULTILINE_ENABLE_DEFAULT` - enable multiline logging for all containers when using the multiline adapter (default `true`) 200 | * `MULTILINE_MATCH` - determines which lines the pattern should match, one of first|last|nonfirst|nonlast, for details see: [MULTILINE_MATCH](#multiline_match) (default `nonfirst`) 201 | * `MULTILINE_PATTERN` - pattern for multiline logging, see: [MULTILINE_MATCH](#multiline_match) (default: `^\s`) 202 | * `MULTILINE_FLUSH_AFTER` - maximum time between the first and last lines of a multiline log entry in milliseconds (default: 500) 203 | * `MULTILINE_SEPARATOR` - separator between lines for output (default: `\n`) 204 | 205 | #### Raw Format 206 | 207 | The raw adapter has a function `toJSON` that can be used to format the message/fields to generate JSON-like output in a simple way, or full JSON output. 208 | 209 | The RAW_FORMAT env variable is used as a [Go template](https://golang.org/pkg/text/template/) with a [`Message` struct](https://github.com/gliderlabs/logspout/blob/master/router/types.go#L52) passed as data. You can access the following fields 210 | 211 | * `Source` - source stream name ("stdout", "stderr", ...) 212 | * `Data` - original log message 213 | * `Time` - a Go [`Time` struct](https://golang.org/pkg/time/#Time) 214 | * `Container` - a [go-dockerclient](https://github.com/fsouza/go-dockerclient) `Container` struct (see [container.go](https://github.com/fsouza/go-dockerclient/blob/master/container.go#L443) source file for accessible fields) 215 | 216 | 217 | Use examples: 218 | 219 | ##### Mixed JSON + generic: 220 | ``` 221 | {{ .Time.Format "2006-01-02T15:04:05Z07:00" }} { "container" : "{{ .Container.Name }}", "labels": {{ toJSON .Container.Config.Labels }}, "timestamp": "{{ .Time.Format "2006-01-02T15:04:05Z07:00" }}", "source" : "{{ .Source }}", "message": {{ toJSON .Data }} } 222 | ``` 223 | 224 | ``` 225 | 2017-10-26T11:59:32Z { "container" : "/catalogo_worker_1", "image": "sha256:e9bce6c17c80c603c4c8dbac2ad2285982d218f6ea0332f8b0fb84572941b773", "labels": {"com.docker.compose.config-hash":"4f9c3d3bfb2f65e29a4bc8a4a1b3f0a1c8a42323106a5e9106fe9279f8031321","com.docker.compose.container-number":"1","com.docker.compose.oneoff":"False","com.docker.compose.project":"catalogo","com.docker.compose.service":"worker","com.docker.compose.version":"1.16.1","logging":"true"}, "timestamp": "2017-10-26T11:59:32Z", "source" : "stdout", "message": "2017-10-26 11:59:32,950 INFO success: command_bus_0 entered RUNNING state, process has stayed up for \u003e than 1 seconds (startsecs)" } 226 | ``` 227 | 228 | ##### Full JSON like: 229 | 230 | ``` 231 | { "container" : "{{ .Container.Name }}", "labels": {{ toJSON .Container.Config.Labels }}, "timestamp": "{{ .Time.Format "2006-01-02T15:04:05Z07:00" }}", "source" : "{{ .Source }}", "message": {{ toJSON .Data }} } 232 | ``` 233 | 234 | ```json 235 | { 236 | "container": "/a_container", 237 | "image": "sha256:e9bce6c17c80c603c4c8dbac2ad2285982d218f6ea0332f8b0fb84572941b773", 238 | "labels": { 239 | "com.docker.compose.config-hash": "4f9c3d3bfb2f65e29a4bc8a4a1b3f0a1c8a42323106a5e9106fe9279f8031321", 240 | "com.docker.compose.container-number": "1", 241 | "com.docker.compose.oneoff": "False", 242 | "com.docker.compose.project": "a_project", 243 | "com.docker.compose.service": "worker", 244 | "com.docker.compose.version": "1.16.1", 245 | "logging": "true" 246 | }, 247 | "timestamp": "2017-10-26T11:59:32Z", 248 | "source": "stdout", 249 | "message": "2017-10-26 11:59:32,950 INFO success: command_bus_0 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)" 250 | } 251 | 252 | ``` 253 | 254 | #### Syslog TCP Framing 255 | 256 | When using a TCP or TLS transport with the Syslog adapter, it is possible to add octet-counting to the emitted frames as described in [RFC6587 (Syslog over TCP) 3.4.1](https://tools.ietf.org/html/rfc6587#section-3.4.1) and [RFC5424 (Syslog over TLS)](https://tools.ietf.org/html/rfc5424). 257 | 258 | This prefixes each message with the length of the message to allow consumers to easily determine where the message ends (rather than traditional LF framing). This also enables multiline Syslog messages without escaping. 259 | 260 | To enable octet-counted framing for Syslog over TCP or TLS, use the `SYSLOG_TCP_FRAMING` environment variable: 261 | 262 | $ docker run --name="logspout" \ 263 | -e SYSLOG_TCP_FRAMING=octet-counted \ 264 | --volume=/var/run/docker.sock:/var/run/docker.sock \ 265 | gliderlabs/logspout \ 266 | syslog+tcp://logs.papertrailapp.com:55555 267 | 268 | > NOTE: The default is to use traditional LF framing for backwards compatibility though octet-counted framing is preferred when it is known the downstream consumer can handle it. 269 | 270 | #### Using Logspout in a swarm 271 | 272 | In a swarm, logspout is best deployed as a global service. When running logspout with 'docker run', you can change the value of the hostname field using the `SYSLOG_HOSTNAME` environment variable as explained above. However, this does not work in a compose file because the value for `SYSLOG_HOSTNAME` will be the same for all logspout "tasks", regardless of the docker host on which they run. To support this mode of deployment, the syslog adapter will look for the file `/etc/host_hostname` and, if the file exists and it is not empty, will configure the hostname field with the content of this file. You can then use a volume mount to map a file on the docker hosts with the file `/etc/host_hostname` in the container. The sample compose file below illustrates how this can be done 273 | 274 | ```yml 275 | version: "3" 276 | networks: 277 | logging: 278 | services: 279 | logspout: 280 | image: gliderlabs/logspout:latest 281 | networks: 282 | - logging 283 | volumes: 284 | - /etc/hostname:/etc/host_hostname:ro 285 | - /var/run/docker.sock:/var/run/docker.sock 286 | command: 287 | syslog://svt2-logger.am2.cloudra.local:514 288 | deploy: 289 | mode: global 290 | resources: 291 | limits: 292 | cpus: '0.20' 293 | memory: 256M 294 | reservations: 295 | cpus: '0.10' 296 | memory: 128M 297 | ``` 298 | 299 | logspout can then be deployed as a global service in the swarm with the following command 300 | 301 | ```bash 302 | docker stack deploy --compose-file STACK 303 | ``` 304 | 305 | More information about services and their mode of deployment can be found here: 306 | https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/ 307 | 308 | ### TLS Settings 309 | logspout supports modification of the client TLS settings via environment variables described below: 310 | 311 | | Environment Variable | Description | 312 | | :--- | :--- | 313 | | `LOGSPOUT_TLS_DISABLE_SYSTEM_ROOTS` | when set to `true` it disables loading the system trust store into the trust store of logspout | 314 | | `LOGSPOUT_TLS_CA_CERTS` | a comma separated list of filesystem paths to pem encoded CA certificates that should be added to logspout's TLS trust store. Each pem file can contain more than one certificate | 315 | | `LOGSPOUT_TLS_CLIENT_CERT` | filesystem path to pem encoded x509 client certificate to load when TLS mutual authentication is desired | 316 | | `LOGSPOUT_TLS_CLIENT_KEY` | filesystem path to pem encoded client private key to load when TLS mutual authentication is desired | 317 | | `LOGSPOUT_TLS_HARDENING` | when set to `true` it enables stricter client TLS settings designed to mitigate some known TLS vulnerabilities | 318 | 319 | #### Example TLS settings 320 | The following settings cover some common use cases. 321 | When running docker, use the `-e` flag to supply environment variables 322 | 323 | **add your own CAs to the list of trusted authorities** 324 | ``` 325 | export LOGSPOUT_TLS_CA_CERTS="/opt/tls/ca/myRootCA1.pem,/opt/tls/ca/myRootCA2.pem" 326 | ``` 327 | 328 | **force logspout to ONLY trust your own CA** 329 | ``` 330 | export LOGSPOUT_TLS_DISABLE_SYSTEM_ROOTS=true 331 | export LOGSPOUT_TLS_CA_CERTS="/opt/tls/ca/myRootCA1.pem" 332 | ``` 333 | 334 | **configure client authentication** 335 | ``` 336 | export LOGSPOUT_TLS_CLIENT_CERT="/opt/tls/client/myClient.pem" 337 | export LOGSPOUT_TLS_CLIENT_KEY="/opt/tls/client/myClient-key.pem" 338 | ``` 339 | 340 | **highest possible security settings (paranoid mode)** 341 | ``` 342 | export LOGSPOUT_TLS_DISABLE_SYSTEM_ROOTS=true 343 | export LOGSPOUT_TLS_HARDENING=true 344 | export LOGSPOUT_TLS_CA_CERTS="/opt/tls/ca/myRootCA1.pem" 345 | export LOGSPOUT_TLS_CLIENT_CERT="/opt/tls/client/myClient.pem" 346 | export LOGSPOUT_TLS_CLIENT_KEY="/opt/tls/client/myClient-key.pem" 347 | ``` 348 | 349 | ## Modules 350 | 351 | The standard distribution of logspout comes with all modules defined in this repository. You can remove or add new modules with custom builds of logspout. In the `custom` dir, edit the `modules.go` file and do a `docker build`. 352 | 353 | ### Builtin modules 354 | 355 | * adapters/raw 356 | * adapters/syslog 357 | * transports/tcp 358 | * transports/tls 359 | * transports/udp 360 | * httpstream 361 | * routesapi 362 | 363 | ### Third-party modules 364 | 365 | * [logspout-kafka](https://github.com/dylanmei/logspout-kafka) 366 | * logspout-redis... 367 | * [logspout-logstash](https://github.com/looplab/logspout-logstash) 368 | * [logspout-redis-logstash](https://github.com/rtoma/logspout-redis-logstash) 369 | * [logspout-gelf](https://github.com/micahhausler/logspout-gelf) for Graylog 370 | * [logspout-fluentd](https://github.com/dsouzajude/logspout-fluentd) for fluentd or fluent-bit - instead of using fluentd log driver 371 | * [logspout-slack](https://github.com/kalisio/logspout-slack) for [Slack](https://slack.com/) notifications 372 | 373 | ### Loggly support 374 | 375 | Use logspout to stream your docker logs to Loggly via the [Loggly syslog endpoint](https://www.loggly.com/docs/streaming-syslog-without-using-files/). 376 | ``` 377 | $ docker run --name logspout -d --volume=/var/run/docker.sock:/var/run/docker.sock \ 378 | -e SYSLOG_STRUCTURED_DATA="@41058 tag=\"some tag name\"" \ 379 | gliderlabs/logspout \ 380 | syslog+tcp://logs-01.loggly.com:514 381 | ``` 382 | 383 | ## Contributing 384 | 385 | As usual, pull requests are welcome. You can also propose releases by opening a PR against the `release` branch from `master`. Please be sure to bump the version and update `CHANGELOG.md` and include your changelog text in the PR body. 386 | 387 | Discuss logspout development with us on Freenode in `#gliderlabs`. 388 | 389 | ## Sponsor 390 | 391 | This project was made possible by [DigitalOcean](http://digitalocean.com) and [Deis](http://deis.io). 392 | 393 | ## License 394 | 395 | BSD 396 | 397 | -------------------------------------------------------------------------------- /SPONSORS: -------------------------------------------------------------------------------- 1 | DigitalOcean http://digitalocean.com 2 | Deis Project http://deis.io 3 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v3.2.14 2 | -------------------------------------------------------------------------------- /adapters/multiline/multiline.go: -------------------------------------------------------------------------------- 1 | package multiline 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | docker "github.com/fsouza/go-dockerclient" 13 | 14 | "github.com/gliderlabs/logspout/router" 15 | ) 16 | 17 | const ( 18 | matchFirst = "first" 19 | matchLast = "last" 20 | matchNonFirst = "nonfirst" 21 | matchNonLast = "nonlast" 22 | defaultFlushAfter = 500 * time.Millisecond 23 | ) 24 | 25 | func init() { 26 | router.AdapterFactories.Register(NewMultilineAdapter, "multiline") 27 | } 28 | 29 | // Adapter collects multi-lint log entries and sends them to the next adapter as a single entry 30 | type Adapter struct { 31 | out chan *router.Message 32 | subAdapter router.LogAdapter 33 | enableByDefault bool 34 | pattern *regexp.Regexp 35 | separator string 36 | matchFirstLine bool 37 | negateMatch bool 38 | flushAfter time.Duration 39 | checkInterval time.Duration 40 | buffers map[string]*router.Message 41 | nextCheck <-chan time.Time 42 | } 43 | 44 | // NewMultilineAdapter returns a configured multiline.Adapter 45 | func NewMultilineAdapter(route *router.Route) (a router.LogAdapter, err error) { //nolint:gocyclo 46 | enableByDefault := true 47 | enableStr := os.Getenv("MULTILINE_ENABLE_DEFAULT") 48 | if enableStr != "" { 49 | enableByDefault, err = strconv.ParseBool(enableStr) 50 | if err != nil { 51 | return nil, errors.New("multiline: invalid value for MULTILINE_ENABLE_DEFAULT (must be true|false): " + enableStr) 52 | } 53 | } 54 | 55 | pattern := os.Getenv("MULTILINE_PATTERN") 56 | if pattern == "" { 57 | pattern = `^\s` 58 | } 59 | 60 | separator := os.Getenv("MULTILINE_SEPARATOR") 61 | if separator == "" { 62 | separator = "\n" 63 | } 64 | patternRegexp, err := regexp.Compile(pattern) 65 | if err != nil { 66 | return nil, errors.New("multiline: invalid value for MULTILINE_PATTERN (must be regexp): " + pattern) 67 | } 68 | 69 | matchType := os.Getenv("MULTILINE_MATCH") 70 | if matchType == "" { 71 | matchType = matchNonFirst 72 | } 73 | matchType = strings.ToLower(matchType) 74 | matchFirstLine := false 75 | negateMatch := false 76 | switch matchType { 77 | case matchFirst: 78 | matchFirstLine = true 79 | negateMatch = false 80 | case matchLast: 81 | matchFirstLine = false 82 | negateMatch = false 83 | case matchNonFirst: 84 | matchFirstLine = true 85 | negateMatch = true 86 | case matchNonLast: 87 | matchFirstLine = false 88 | negateMatch = true 89 | default: 90 | return nil, errors.New("multiline: invalid value for MULTILINE_MATCH (must be one of first|last|nonfirst|nonlast): " + matchType) 91 | } 92 | 93 | flushAfter := defaultFlushAfter 94 | flushAfterStr := os.Getenv("MULTILINE_FLUSH_AFTER") 95 | if flushAfterStr != "" { 96 | timeoutMS, errConv := strconv.Atoi(flushAfterStr) 97 | if errConv != nil { 98 | return nil, errors.New("multiline: invalid value for multiline_timeout (must be number): " + flushAfterStr) 99 | } 100 | flushAfter = time.Duration(timeoutMS) * time.Millisecond 101 | } 102 | 103 | parts := strings.SplitN(route.Adapter, "+", 2) 104 | if len(parts) != 2 { //nolint:gomnd 105 | return nil, errors.New("multiline: adapter must have a sub-adapter, eg: multiline+raw+tcp") 106 | } 107 | 108 | originalAdapter := route.Adapter 109 | route.Adapter = parts[1] 110 | factory, found := router.AdapterFactories.Lookup(route.AdapterType()) 111 | if !found { 112 | return nil, errors.New("bad adapter: " + originalAdapter) 113 | } 114 | subAdapter, err := factory(route) 115 | if err != nil { 116 | return nil, err 117 | } 118 | route.Adapter = originalAdapter 119 | 120 | out := make(chan *router.Message) 121 | checkInterval := flushAfter / 2 //nolint:gomnd 122 | 123 | return &Adapter{ 124 | out: out, 125 | subAdapter: subAdapter, 126 | enableByDefault: enableByDefault, 127 | pattern: patternRegexp, 128 | separator: separator, 129 | matchFirstLine: matchFirstLine, 130 | negateMatch: negateMatch, 131 | flushAfter: flushAfter, 132 | checkInterval: checkInterval, 133 | buffers: make(map[string]*router.Message), 134 | nextCheck: time.After(checkInterval), 135 | }, nil 136 | } 137 | 138 | // Stream sends log data to the next adapter 139 | func (a *Adapter) Stream(logstream chan *router.Message) { //nolint:gocyclo,gocognit 140 | wg := sync.WaitGroup{} 141 | wg.Add(1) 142 | go func() { 143 | a.subAdapter.Stream(a.out) 144 | wg.Done() 145 | }() 146 | defer func() { 147 | for _, message := range a.buffers { 148 | a.out <- message 149 | } 150 | 151 | close(a.out) 152 | wg.Wait() 153 | }() 154 | 155 | for { 156 | select { 157 | case message, ok := <-logstream: 158 | if !ok { 159 | return 160 | } 161 | 162 | if !multilineContainer(message.Container, a.enableByDefault) { 163 | a.out <- message 164 | continue 165 | } 166 | 167 | cID := message.Container.ID 168 | old, oldExists := a.buffers[cID] 169 | if a.isFirstLine(message) { //nolint:nestif 170 | if oldExists { 171 | a.out <- old 172 | } 173 | 174 | a.buffers[cID] = message 175 | } else { 176 | isLastLine := a.isLastLine(message) 177 | 178 | if oldExists { 179 | old.Data += a.separator + message.Data 180 | message = old 181 | } 182 | 183 | if isLastLine { 184 | a.out <- message 185 | if oldExists { 186 | delete(a.buffers, cID) 187 | } 188 | } else { 189 | a.buffers[cID] = message 190 | } 191 | } 192 | case <-a.nextCheck: 193 | now := time.Now() 194 | 195 | for key, message := range a.buffers { 196 | if message.Time.Add(a.flushAfter).After(now) { 197 | a.out <- message 198 | delete(a.buffers, key) 199 | } 200 | } 201 | 202 | a.nextCheck = time.After(a.checkInterval) 203 | } 204 | } 205 | } 206 | 207 | func (a *Adapter) isFirstLine(message *router.Message) bool { 208 | if !a.matchFirstLine { 209 | return false 210 | } 211 | 212 | match := a.pattern.MatchString(message.Data) 213 | if a.negateMatch { 214 | return !match 215 | } 216 | 217 | return match 218 | } 219 | 220 | func (a *Adapter) isLastLine(message *router.Message) bool { 221 | if a.matchFirstLine { 222 | return false 223 | } 224 | 225 | match := a.pattern.MatchString(message.Data) 226 | if a.negateMatch { 227 | return !match 228 | } 229 | 230 | return match 231 | } 232 | 233 | func multilineContainer(container *docker.Container, def bool) bool { 234 | for _, kv := range container.Config.Env { 235 | kvp := strings.SplitN(kv, "=", 2) 236 | if len(kvp) == 2 && kvp[0] == "LOGSPOUT_MULTILINE" { 237 | switch strings.ToLower(kvp[1]) { 238 | case "true": 239 | return true 240 | case "false": 241 | return false 242 | } 243 | return def 244 | } 245 | } 246 | 247 | return def 248 | } 249 | -------------------------------------------------------------------------------- /adapters/multiline/multiline_test.go: -------------------------------------------------------------------------------- 1 | package multiline 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | docker "github.com/fsouza/go-dockerclient" 11 | 12 | "github.com/gliderlabs/logspout/router" 13 | ) 14 | 15 | type dummyAdapter struct { 16 | messages []*router.Message 17 | *sync.WaitGroup 18 | } 19 | 20 | type testData struct { 21 | input []string 22 | expected []string 23 | pattern *regexp.Regexp 24 | matchFirstLine bool 25 | negateMatch bool 26 | } 27 | 28 | type envTestData struct { 29 | env []string 30 | def bool 31 | expected bool 32 | } 33 | 34 | func (da *dummyAdapter) Stream(logstream chan *router.Message) { 35 | for m := range logstream { 36 | da.messages = append(da.messages, m) 37 | } 38 | da.Done() 39 | } 40 | 41 | func TestMultiline(t *testing.T) { 42 | tests := []*testData{ 43 | { 44 | input: []string{ 45 | "some", 46 | " multi", 47 | " line", 48 | "other", 49 | " multiline", 50 | }, 51 | expected: []string{ 52 | "some\n multi\n line", 53 | "other\n multiline", 54 | }, 55 | pattern: regexp.MustCompile(`^\s`), 56 | matchFirstLine: true, 57 | negateMatch: true, 58 | }, 59 | { 60 | input: []string{ 61 | "some:", 62 | "multi", 63 | "line", 64 | "other:", 65 | "multiline", 66 | }, 67 | expected: []string{ 68 | "some:\nmulti\nline", 69 | "other:\nmultiline", 70 | }, 71 | pattern: regexp.MustCompile(`:$`), 72 | matchFirstLine: true, 73 | negateMatch: false, 74 | }, 75 | { 76 | input: []string{ 77 | "some$", 78 | "multi$", 79 | "line", 80 | "other$", 81 | "multiline", 82 | }, 83 | expected: []string{ 84 | "some$\nmulti$\nline", 85 | "other$\nmultiline", 86 | }, 87 | pattern: regexp.MustCompile(`\$$`), 88 | matchFirstLine: false, 89 | negateMatch: true, 90 | }, 91 | { 92 | input: []string{ 93 | "some", 94 | "multi", 95 | "line!", 96 | "other", 97 | "multiline!", 98 | }, 99 | expected: []string{ 100 | "some\nmulti\nline!", 101 | "other\nmultiline!", 102 | }, 103 | pattern: regexp.MustCompile(`!$`), 104 | matchFirstLine: false, 105 | negateMatch: false, 106 | }, 107 | { 108 | input: []string{ 109 | "not yet", 110 | "Traceback", 111 | " tb1", 112 | " tb2!", 113 | "Error123", 114 | "no more traceback", 115 | "STATEMENT:", 116 | " still statement", 117 | "end of statement", 118 | "no more statement", 119 | }, 120 | expected: []string{ 121 | "not yet", 122 | "Traceback\n tb1\n tb2!\nError123", 123 | "no more traceback", 124 | "STATEMENT:\n still statement\nend of statement", 125 | "no more statement", 126 | }, 127 | pattern: regexp.MustCompile(`^(DETAIL:|STATEMENT:|Traceback|\s)`), 128 | matchFirstLine: false, 129 | negateMatch: true, 130 | }, 131 | } 132 | 133 | for _, test := range tests { 134 | in := make(chan *router.Message) 135 | out := make(chan *router.Message) 136 | container := &docker.Container{ 137 | ID: "test", 138 | Config: &docker.Config{}, 139 | } 140 | 141 | da := &dummyAdapter{make([]*router.Message, 0), &sync.WaitGroup{}} 142 | da.Add(1) 143 | 144 | ma := &Adapter{ 145 | out: out, 146 | subAdapter: da, 147 | enableByDefault: true, 148 | pattern: test.pattern, 149 | matchFirstLine: test.matchFirstLine, 150 | negateMatch: test.negateMatch, 151 | flushAfter: time.Second * 10, 152 | checkInterval: time.Millisecond * 100, 153 | buffers: make(map[string]*router.Message), 154 | nextCheck: time.After(time.Millisecond * 100), 155 | separator: "\n", 156 | } 157 | 158 | go ma.Stream(in) 159 | 160 | for _, i := range test.input { 161 | in <- &router.Message{ 162 | Container: container, 163 | Data: i, 164 | Source: "stdout", 165 | Time: time.Now(), 166 | } 167 | } 168 | 169 | close(in) 170 | da.Wait() 171 | 172 | for i, m := range da.messages { 173 | if m.Data != test.expected[i] { 174 | t.Errorf("Expected: '%v', Got: '%v'", replaceNewLines(test.expected[i]), replaceNewLines(m.Data)) 175 | } 176 | } 177 | } 178 | } 179 | 180 | func TestContainerEnv(t *testing.T) { 181 | tests := []envTestData{ 182 | { 183 | def: true, 184 | env: []string{}, 185 | expected: true, 186 | }, 187 | { 188 | def: false, 189 | env: []string{}, 190 | expected: false, 191 | }, 192 | { 193 | def: true, 194 | env: []string{"LOGSPOUT_MULTILINE=true"}, 195 | expected: true, 196 | }, 197 | { 198 | def: false, 199 | env: []string{"LOGSPOUT_MULTILINE=true"}, 200 | expected: true, 201 | }, 202 | { 203 | def: true, 204 | env: []string{"LOGSPOUT_MULTILINE=false"}, 205 | expected: false, 206 | }, 207 | { 208 | def: false, 209 | env: []string{"LOGSPOUT_MULTILINE=false"}, 210 | expected: false, 211 | }, 212 | } 213 | 214 | for _, test := range tests { 215 | container := &docker.Container{ 216 | ID: "test", 217 | Config: &docker.Config{ 218 | Env: test.env, 219 | }, 220 | } 221 | 222 | result := multilineContainer(container, test.def) 223 | 224 | if result != test.expected { 225 | t.Errorf("Expected: %v, Got: %v, env: %v", test.expected, result, test.env) 226 | } 227 | } 228 | } 229 | 230 | func replaceNewLines(str string) string { 231 | return strings.Replace(str, "\n", "\\n", -1) 232 | } 233 | -------------------------------------------------------------------------------- /adapters/raw/raw.go: -------------------------------------------------------------------------------- 1 | package raw 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | "net" 9 | "os" 10 | "text/template" 11 | 12 | "github.com/gliderlabs/logspout/router" 13 | ) 14 | 15 | func init() { 16 | router.AdapterFactories.Register(NewRawAdapter, "raw") 17 | } 18 | 19 | var funcs = template.FuncMap{ 20 | "toJSON": func(value interface{}) string { 21 | bytes, err := json.Marshal(value) 22 | if err != nil { 23 | log.Println("error marshaling to JSON: ", err) 24 | return "null" 25 | } 26 | return string(bytes) 27 | }, 28 | } 29 | 30 | // NewRawAdapter returns a configured raw.Adapter 31 | func NewRawAdapter(route *router.Route) (router.LogAdapter, error) { 32 | transport, found := router.AdapterTransports.Lookup(route.AdapterTransport("udp")) 33 | if !found { 34 | return nil, errors.New("bad transport: " + route.Adapter) 35 | } 36 | conn, err := transport.Dial(route.Address, route.Options) 37 | if err != nil { 38 | return nil, err 39 | } 40 | tmplStr := "{{.Data}}\n" 41 | if os.Getenv("RAW_FORMAT") != "" { 42 | tmplStr = os.Getenv("RAW_FORMAT") 43 | } 44 | tmpl, err := template.New("raw").Funcs(funcs).Parse(tmplStr) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &Adapter{ 49 | route: route, 50 | conn: conn, 51 | tmpl: tmpl, 52 | }, nil 53 | } 54 | 55 | // Adapter is a simple adapter that streams log output to a connection without any templating 56 | type Adapter struct { 57 | conn net.Conn 58 | route *router.Route 59 | tmpl *template.Template 60 | } 61 | 62 | // Stream sends log data to a connection 63 | func (a *Adapter) Stream(logstream chan *router.Message) { 64 | for message := range logstream { 65 | buf := new(bytes.Buffer) 66 | err := a.tmpl.Execute(buf, message) 67 | if err != nil { 68 | log.Println("raw:", err) 69 | return 70 | } 71 | _, err = a.conn.Write(buf.Bytes()) 72 | if err != nil { 73 | log.Println("raw:", err) 74 | if _, ok := a.conn.(*net.UDPConn); !ok { 75 | return 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /adapters/syslog/syslog.go: -------------------------------------------------------------------------------- 1 | package syslog 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "log/syslog" 11 | "net" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "syscall" 16 | "text/template" 17 | "time" 18 | 19 | "github.com/gliderlabs/logspout/cfg" 20 | "github.com/gliderlabs/logspout/router" 21 | ) 22 | 23 | const ( 24 | // Rfc5424Format is the modern syslog protocol format. https://tools.ietf.org/html/rfc5424 25 | Rfc5424Format Format = "rfc5424" 26 | // Rfc3164Format is the legacy BSD syslog protocol format. https://tools.ietf.org/html/rfc3164 27 | Rfc3164Format Format = "rfc3164" 28 | 29 | // TraditionalTCPFraming is the traditional LF framing of syslog messages on the wire 30 | TraditionalTCPFraming TCPFraming = "traditional" 31 | // OctetCountedTCPFraming prepends the size of each message before the message. https://tools.ietf.org/html/rfc6587#section-3.4.1 32 | OctetCountedTCPFraming TCPFraming = "octet-counted" 33 | 34 | defaultFormat = Rfc5424Format 35 | defaultTCPFraming = TraditionalTCPFraming 36 | defaultRetryCount = 10 37 | ) 38 | 39 | var ( 40 | hostname string 41 | ) 42 | 43 | // Format represents the RFC spec to use for syslog messages 44 | type Format string 45 | 46 | // TCPFraming represents the type of framing to use for syslog messages 47 | type TCPFraming string 48 | 49 | func init() { 50 | hostname, _ = os.Hostname() 51 | router.AdapterFactories.Register(NewSyslogAdapter, "syslog") 52 | } 53 | 54 | func debug(v ...interface{}) { 55 | if os.Getenv("DEBUG") != "" { 56 | log.Println(v...) 57 | } 58 | } 59 | 60 | func getFormat() (Format, error) { 61 | switch s := cfg.GetEnvDefault("SYSLOG_FORMAT", string(defaultFormat)); s { 62 | case string(Rfc5424Format): 63 | return Rfc5424Format, nil 64 | case string(Rfc3164Format): 65 | return Rfc3164Format, nil 66 | default: 67 | return defaultFormat, fmt.Errorf("unknown SYSLOG_FORMAT value: %s", s) 68 | } 69 | } 70 | 71 | func getHostname() string { 72 | content, err := ioutil.ReadFile("/etc/host_hostname") 73 | if err == nil && len(content) > 0 { 74 | hostname = strings.TrimRight(string(content), "\r\n") 75 | } else { 76 | hostname = cfg.GetEnvDefault("SYSLOG_HOSTNAME", "{{.Container.Config.Hostname}}") 77 | } 78 | return hostname 79 | } 80 | 81 | func getFieldTemplates(route *router.Route) (*FieldTemplates, error) { 82 | var err error 83 | var s string 84 | var tmpl FieldTemplates 85 | 86 | s = cfg.GetEnvDefault("SYSLOG_PRIORITY", "{{.Priority}}") 87 | if tmpl.priority, err = template.New("priority").Parse(s); err != nil { 88 | return nil, err 89 | } 90 | debug("setting priority to:", s) 91 | 92 | s = cfg.GetEnvDefault("SYSLOG_TIMESTAMP", "{{.Timestamp}}") 93 | if tmpl.timestamp, err = template.New("timestamp").Parse(s); err != nil { 94 | return nil, err 95 | } 96 | debug("setting timestamp to:", s) 97 | 98 | s = getHostname() 99 | if tmpl.hostname, err = template.New("hostname").Parse(s); err != nil { 100 | return nil, err 101 | } 102 | debug("setting hostname to:", s) 103 | 104 | s = cfg.GetEnvDefault("SYSLOG_TAG", "{{.ContainerName}}"+route.Options["append_tag"]) 105 | if tmpl.tag, err = template.New("tag").Parse(s); err != nil { 106 | return nil, err 107 | } 108 | debug("setting tag to:", s) 109 | 110 | s = cfg.GetEnvDefault("SYSLOG_PID", "{{.Container.State.Pid}}") 111 | if tmpl.pid, err = template.New("pid").Parse(s); err != nil { 112 | return nil, err 113 | } 114 | debug("setting pid to:", s) 115 | 116 | s = cfg.GetEnvDefault("SYSLOG_STRUCTURED_DATA", "") 117 | if route.Options["structured_data"] != "" { 118 | s = route.Options["structured_data"] 119 | } 120 | if s == "" { 121 | s = "-" 122 | } else { 123 | s = fmt.Sprintf("[%s]", s) 124 | } 125 | if tmpl.structuredData, err = template.New("structuredData").Parse(s); err != nil { 126 | return nil, err 127 | } 128 | debug("setting structuredData to:", s) 129 | 130 | s = cfg.GetEnvDefault("SYSLOG_DATA", "{{.Data}}") 131 | if tmpl.data, err = template.New("data").Parse(s); err != nil { 132 | return nil, err 133 | } 134 | debug("setting data to:", s) 135 | 136 | return &tmpl, nil 137 | } 138 | 139 | func getTCPFraming() (TCPFraming, error) { 140 | switch s := cfg.GetEnvDefault("SYSLOG_TCP_FRAMING", string(defaultTCPFraming)); s { 141 | case string(TraditionalTCPFraming): 142 | return TraditionalTCPFraming, nil 143 | case string(OctetCountedTCPFraming): 144 | return OctetCountedTCPFraming, nil 145 | default: 146 | return defaultTCPFraming, fmt.Errorf("unknown SYSLOG_TCP_FRAMING value: %s", s) 147 | } 148 | } 149 | 150 | func getRetryCount() uint { 151 | retryCountStr := cfg.GetEnvDefault("RETRY_COUNT", "") 152 | if retryCountStr != "" { 153 | retryCount, _ := strconv.Atoi(retryCountStr) 154 | return uint(retryCount) 155 | } 156 | return defaultRetryCount 157 | } 158 | 159 | func isTCPConnection(conn net.Conn) bool { 160 | switch conn.(type) { 161 | case *net.TCPConn: 162 | return true 163 | case *tls.Conn: 164 | return true 165 | default: 166 | return false 167 | } 168 | } 169 | 170 | // NewSyslogAdapter returnas a configured syslog.Adapter 171 | func NewSyslogAdapter(route *router.Route) (router.LogAdapter, error) { 172 | transport, found := router.AdapterTransports.Lookup(route.AdapterTransport("udp")) 173 | if !found { 174 | return nil, errors.New("bad transport: " + route.Adapter) 175 | } 176 | conn, err := transport.Dial(route.Address, route.Options) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | format, err := getFormat() 182 | if err != nil { 183 | return nil, err 184 | } 185 | debug("setting format to:", format) 186 | 187 | tmpl, err := getFieldTemplates(route) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | connIsTCP := isTCPConnection(conn) 193 | debug("setting connIsTCP to:", connIsTCP) 194 | 195 | var tcpFraming TCPFraming 196 | if connIsTCP { 197 | if tcpFraming, err = getTCPFraming(); err != nil { 198 | return nil, err 199 | } 200 | debug("setting tcpFraming to:", tcpFraming) 201 | } 202 | 203 | retryCount := getRetryCount() 204 | debug("setting retryCount to:", retryCount) 205 | 206 | return &Adapter{ 207 | route: route, 208 | conn: conn, 209 | connIsTCP: connIsTCP, 210 | format: format, 211 | tmpl: tmpl, 212 | transport: transport, 213 | tcpFraming: tcpFraming, 214 | retryCount: retryCount, 215 | }, nil 216 | } 217 | 218 | // FieldTemplates for rendering Syslog messages 219 | type FieldTemplates struct { 220 | priority *template.Template 221 | timestamp *template.Template 222 | hostname *template.Template 223 | tag *template.Template 224 | pid *template.Template 225 | structuredData *template.Template 226 | data *template.Template 227 | } 228 | 229 | // Adapter streams log output to a connection in the Syslog format 230 | type Adapter struct { 231 | conn net.Conn 232 | connIsTCP bool 233 | route *router.Route 234 | format Format 235 | tmpl *FieldTemplates 236 | transport router.AdapterTransport 237 | tcpFraming TCPFraming 238 | retryCount uint 239 | } 240 | 241 | // Stream sends log data to a connection 242 | func (a *Adapter) Stream(logstream chan *router.Message) { 243 | for message := range logstream { 244 | m := &Message{message} 245 | buf, err := m.Render(a.format, a.tmpl) 246 | if err != nil { 247 | log.Println("syslog:", err) 248 | return 249 | } 250 | 251 | if a.connIsTCP && a.tcpFraming == OctetCountedTCPFraming { 252 | buf = append([]byte(fmt.Sprintf("%d ", len(buf))), buf...) 253 | } 254 | 255 | if _, err = a.conn.Write(buf); err != nil { 256 | log.Println("syslog:", err) 257 | if a.connIsTCP { 258 | if err = a.retry(buf, err); err != nil { 259 | log.Panicf("syslog retry err: %+v", err) 260 | return 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | func (a *Adapter) retry(buf []byte, err error) error { 268 | if opError, ok := err.(*net.OpError); ok { 269 | if (opError.Temporary() && !errors.Is(opError, syscall.ECONNRESET)) || opError.Timeout() { 270 | retryErr := a.retryTemporary(buf) 271 | if retryErr == nil { 272 | return nil 273 | } 274 | } 275 | } 276 | if reconnErr := a.reconnect(); reconnErr != nil { 277 | return reconnErr 278 | } 279 | if _, err = a.conn.Write(buf); err != nil { 280 | log.Println("syslog: reconnect failed") 281 | return err 282 | } 283 | log.Println("syslog: reconnect successful") 284 | return nil 285 | } 286 | 287 | func (a *Adapter) retryTemporary(buf []byte) error { 288 | log.Printf("syslog: retrying tcp up to %v times\n", a.retryCount) 289 | err := retryExp(func() error { 290 | _, err := a.conn.Write(buf) 291 | if err == nil { 292 | log.Println("syslog: retry successful") 293 | return nil 294 | } 295 | 296 | return err 297 | }, a.retryCount) 298 | 299 | if err != nil { 300 | log.Println("syslog: retry failed") 301 | return err 302 | } 303 | 304 | return nil 305 | } 306 | 307 | func (a *Adapter) reconnect() error { 308 | log.Printf("syslog: reconnecting up to %v times\n", a.retryCount) 309 | err := retryExp(func() error { 310 | conn, err := a.transport.Dial(a.route.Address, a.route.Options) 311 | if err != nil { 312 | return err 313 | } 314 | a.conn = conn 315 | return nil 316 | }, a.retryCount) 317 | 318 | if err != nil { 319 | return err 320 | } 321 | return nil 322 | } 323 | 324 | func retryExp(fun func() error, tries uint) error { 325 | var try uint 326 | for { 327 | err := fun() 328 | if err == nil { 329 | return nil 330 | } 331 | 332 | try++ 333 | if try > tries { 334 | return err 335 | } 336 | 337 | time.Sleep((1 << try) * 10 * time.Millisecond) 338 | } 339 | } 340 | 341 | // Message extends router.Message for the syslog standard 342 | type Message struct { 343 | *router.Message 344 | } 345 | 346 | // Render transforms the log message using the Syslog template 347 | func (m *Message) Render(format Format, tmpl *FieldTemplates) ([]byte, error) { 348 | priority := new(bytes.Buffer) 349 | if err := tmpl.priority.Execute(priority, m); err != nil { 350 | return nil, err 351 | } 352 | 353 | timestamp := new(bytes.Buffer) 354 | if err := tmpl.timestamp.Execute(timestamp, m); err != nil { 355 | return nil, err 356 | } 357 | 358 | hostname := new(bytes.Buffer) 359 | if err := tmpl.hostname.Execute(hostname, m); err != nil { 360 | return nil, err 361 | } 362 | 363 | tag := new(bytes.Buffer) 364 | if err := tmpl.tag.Execute(tag, m); err != nil { 365 | return nil, err 366 | } 367 | 368 | pid := new(bytes.Buffer) 369 | if err := tmpl.pid.Execute(pid, m); err != nil { 370 | return nil, err 371 | } 372 | 373 | structuredData := new(bytes.Buffer) 374 | if err := tmpl.structuredData.Execute(structuredData, m); err != nil { 375 | return nil, err 376 | } 377 | 378 | data := new(bytes.Buffer) 379 | if err := tmpl.data.Execute(data, m); err != nil { 380 | return nil, err 381 | } 382 | 383 | buf := new(bytes.Buffer) 384 | switch format { 385 | case Rfc5424Format: 386 | // notes from RFC: 387 | // - there is no upper limit for the entire message and depends on the transport in use 388 | // - the HOSTNAME field must not exceed 255 characters 389 | // - the TAG field must not exceed 48 characters 390 | // - the PROCID field must not exceed 128 characters 391 | fmt.Fprintf(buf, "<%s>1 %s %.255s %.48s %.128s - %s %s\n", 392 | priority, timestamp, hostname, tag, pid, structuredData, data, 393 | ) 394 | case Rfc3164Format: 395 | // notes from RFC: 396 | // - the entire message must be <= 1024 bytes 397 | // - the TAG field must not exceed 32 characters 398 | fmt.Fprintf(buf, "<%s>%s %s %.32s[%s]: %s\n", 399 | priority, timestamp, hostname, tag, pid, data, 400 | ) 401 | } 402 | 403 | return buf.Bytes(), nil 404 | } 405 | 406 | // Priority returns a syslog.Priority based on the message source 407 | func (m *Message) Priority() syslog.Priority { 408 | switch m.Message.Source { 409 | case "stdout": 410 | return syslog.LOG_USER | syslog.LOG_INFO 411 | case "stderr": 412 | return syslog.LOG_USER | syslog.LOG_ERR 413 | default: 414 | return syslog.LOG_DAEMON | syslog.LOG_INFO 415 | } 416 | } 417 | 418 | // Hostname returns the os hostname 419 | func (m *Message) Hostname() string { 420 | return hostname 421 | } 422 | 423 | // Timestamp returns the message's syslog formatted timestamp 424 | func (m *Message) Timestamp() string { 425 | return m.Message.Time.Format(time.RFC3339) 426 | } 427 | 428 | // ContainerName returns the message's container name 429 | func (m *Message) ContainerName() string { 430 | return m.Message.Container.Name[1:] 431 | } 432 | 433 | // ContainerNameSplitN returns the message's container name sliced at most "n" times using "sep" 434 | func (m *Message) ContainerNameSplitN(sep string, n int) []string { 435 | return strings.SplitN(m.ContainerName(), sep, n) 436 | } 437 | -------------------------------------------------------------------------------- /adapters/syslog/syslog_test.go: -------------------------------------------------------------------------------- 1 | package syslog 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "testing" 15 | "time" 16 | 17 | _ "github.com/gliderlabs/logspout/transports/tcp" 18 | _ "github.com/gliderlabs/logspout/transports/tls" 19 | _ "github.com/gliderlabs/logspout/transports/udp" 20 | 21 | docker "github.com/fsouza/go-dockerclient" 22 | 23 | "github.com/gliderlabs/logspout/router" 24 | ) 25 | 26 | const ( 27 | connCloseIdx = 5 28 | ) 29 | 30 | var ( 31 | container = &docker.Container{ 32 | ID: "8dfafdbc3a40", 33 | Name: "\x00container", 34 | Config: &docker.Config{ 35 | Hostname: "8dfafdbc3a40", 36 | }, 37 | } 38 | hostHostnameFilename = "/tmp/host_hostname" 39 | hostnameContent = "hostname" 40 | badHostnameContent = "hostname\r\n" 41 | ) 42 | 43 | func TestSyslogOctetFraming(t *testing.T) { 44 | os.Setenv("SYSLOG_TCP_FRAMING", "octet-counted") 45 | defer os.Unsetenv("SYSLOG_TCP_FRAMING") 46 | 47 | done := make(chan string) 48 | addr, sock, srvWG := startServer("tcp", "", done) 49 | defer srvWG.Wait() 50 | defer os.Remove(addr) 51 | defer sock.Close() 52 | 53 | route := &router.Route{Adapter: "syslog+tcp", Address: addr} 54 | adapter, err := NewSyslogAdapter(route) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | defer adapter.(*Adapter).conn.Close() 59 | 60 | stream := make(chan *router.Message) 61 | go adapter.Stream(stream) 62 | 63 | count := 1 64 | messages := make(chan string, count) 65 | go sendLogstream(stream, messages, adapter, count) 66 | 67 | timeout := time.After(6 * time.Second) 68 | msgnum := 1 69 | select { 70 | case msg := <-done: 71 | sizeStr := "" 72 | _, err := fmt.Sscan(msg, &sizeStr) 73 | if err != nil { 74 | t.Fatal("unable to scan size from message: ", err) 75 | } 76 | 77 | size, err := strconv.ParseInt(sizeStr, 10, 32) 78 | if err != nil { 79 | t.Fatal("unable to scan size from message: ", err) 80 | } 81 | 82 | expectedOctetFrame := len(sizeStr) + 1 + int(size) 83 | if len(msg) != expectedOctetFrame { 84 | t.Errorf("expected octet frame to be %d. got %d instead for message %s", expectedOctetFrame, size, msg) 85 | } 86 | return 87 | case <-timeout: 88 | t.Fatal("timeout after", msgnum, "messages") 89 | return 90 | } 91 | } 92 | 93 | func TestSysLogFormat(t *testing.T) { 94 | defer os.Unsetenv("SYSLOG_FORMAT") 95 | 96 | newFormat := Rfc3164Format 97 | os.Setenv("SYSLOG_FORMAT", string(newFormat)) 98 | format, err := getFormat() 99 | if err != nil { 100 | t.Fatal("unexpected error: ", err) 101 | } 102 | if format != newFormat { 103 | t.Errorf("expected %v got %v", newFormat, format) 104 | } 105 | 106 | os.Unsetenv("SYSLOG_FORMAT") 107 | format, err = getFormat() 108 | if err != nil { 109 | t.Fatal("unexpected error: ", err) 110 | } 111 | if format != defaultFormat { 112 | t.Errorf("expected %v got %v", defaultFormat, format) 113 | } 114 | 115 | os.Setenv("SYSLOG_FORMAT", "invalid-option") 116 | _, err = getFormat() 117 | if err == nil { 118 | t.Fatal("expected error, got nil") 119 | } 120 | } 121 | 122 | func TestSysLogTCPFraming(t *testing.T) { 123 | defer os.Unsetenv("SYSLOG_TCP_FRAMING") 124 | 125 | newTCPFraming := OctetCountedTCPFraming 126 | os.Setenv("SYSLOG_TCP_FRAMING", string(newTCPFraming)) 127 | tcpFraming, err := getTCPFraming() 128 | if err != nil { 129 | t.Fatal("unexpected error: ", err) 130 | } 131 | if tcpFraming != newTCPFraming { 132 | t.Errorf("expected %v got %v", newTCPFraming, tcpFraming) 133 | } 134 | 135 | os.Unsetenv("SYSLOG_TCP_FRAMING") 136 | tcpFraming, err = getTCPFraming() 137 | if err != nil { 138 | t.Fatal("unexpected error: ", err) 139 | } 140 | if tcpFraming != defaultTCPFraming { 141 | t.Errorf("expected %v got %v", defaultTCPFraming, tcpFraming) 142 | } 143 | 144 | os.Setenv("SYSLOG_TCP_FRAMING", "invalid-option") 145 | _, err = getTCPFraming() 146 | if err == nil { 147 | t.Fatal("expected error, got nil") 148 | } 149 | } 150 | 151 | func TestSyslogRetryCount(t *testing.T) { 152 | newRetryCount := uint(20) 153 | os.Setenv("RETRY_COUNT", strconv.Itoa(int(newRetryCount))) 154 | retryCount := getRetryCount() 155 | if retryCount != newRetryCount { 156 | t.Errorf("expected %v got %v", newRetryCount, retryCount) 157 | } 158 | 159 | os.Unsetenv("RETRY_COUNT") 160 | retryCount = getRetryCount() 161 | if retryCount != defaultRetryCount { 162 | t.Errorf("expected %v got %v", defaultRetryCount, retryCount) 163 | } 164 | } 165 | 166 | func TestSyslogReconnectOnClose(t *testing.T) { 167 | done := make(chan string) 168 | addr, sock, srvWG := startServer("tcp", "", done) 169 | defer srvWG.Wait() 170 | defer os.Remove(addr) 171 | defer sock.Close() 172 | route := &router.Route{Adapter: "syslog+tcp", Address: addr} 173 | adapter, err := NewSyslogAdapter(route) 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | stream := make(chan *router.Message) 179 | go adapter.Stream(stream) 180 | 181 | count := 100 182 | messages := make(chan string, count) 183 | go sendLogstream(stream, messages, adapter, count) 184 | 185 | timeout := time.After(6 * time.Second) 186 | msgnum := 1 187 | for { 188 | select { 189 | case msg := <-done: 190 | // Don't check a message that we know was dropped 191 | if msgnum%connCloseIdx == 0 { 192 | <-messages 193 | msgnum++ 194 | } 195 | check(t, <-messages, msg) 196 | msgnum++ 197 | case <-timeout: 198 | adapter.(*Adapter).conn.Close() 199 | t.Fatal("timeout after", msgnum, "messages") 200 | return 201 | default: 202 | if msgnum == count { 203 | adapter.(*Adapter).conn.Close() 204 | return 205 | } 206 | } 207 | } 208 | } 209 | 210 | func TestHostnameDoesNotHaveLineFeed(t *testing.T) { 211 | if err := ioutil.WriteFile(hostHostnameFilename, []byte(badHostnameContent), 0777); err != nil { 212 | t.Fatal(err) 213 | } 214 | testHostname := getHostname() 215 | if strings.Contains(testHostname, badHostnameContent) { 216 | t.Errorf("expected hostname to be %s. got %s in hostname %s", hostnameContent, badHostnameContent, testHostname) 217 | } 218 | } 219 | 220 | func startServer(n, la string, done chan<- string) (addr string, sock io.Closer, wg *sync.WaitGroup) { 221 | if n == "udp" || n == "tcp" { 222 | la = "127.0.0.1:0" 223 | } 224 | wg = new(sync.WaitGroup) 225 | 226 | l, err := net.Listen(n, la) 227 | if err != nil { 228 | log.Fatalf("startServer failed: %v", err) 229 | } 230 | addr = l.Addr().String() 231 | sock = l 232 | wg.Add(1) 233 | go func() { 234 | defer wg.Done() 235 | runStreamSyslog(l, done, wg) 236 | }() 237 | 238 | return 239 | } 240 | 241 | func runStreamSyslog(l net.Listener, done chan<- string, wg *sync.WaitGroup) { 242 | for { 243 | c, err := l.Accept() 244 | if err != nil { 245 | return 246 | } 247 | wg.Add(1) 248 | go func(c net.Conn) { 249 | defer wg.Done() 250 | c.SetReadDeadline(time.Now().Add(5 * time.Second)) 251 | b := bufio.NewReader(c) 252 | var i = 1 253 | for { 254 | i++ 255 | s, err := b.ReadString('\n') 256 | if err != nil { 257 | break 258 | } 259 | done <- s 260 | if i%connCloseIdx == 0 { 261 | break 262 | } 263 | } 264 | c.Close() 265 | }(c) 266 | } 267 | } 268 | 269 | func sendLogstream(stream chan *router.Message, messages chan string, adapter router.LogAdapter, count int) { 270 | for i := 1; i <= count; i++ { 271 | msg := &Message{ 272 | Message: &router.Message{ 273 | Container: container, 274 | Data: "test " + strconv.Itoa(i), 275 | Time: time.Now(), 276 | Source: "stdout", 277 | }, 278 | } 279 | stream <- msg.Message 280 | b, _ := msg.Render(adapter.(*Adapter).format, adapter.(*Adapter).tmpl) 281 | messages <- string(b) 282 | time.Sleep(10 * time.Millisecond) 283 | } 284 | } 285 | 286 | func check(t *testing.T, in string, out string) { 287 | if in != out { 288 | t.Errorf("expected: %s\ngot: %s\n", in, out) 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /build-custom.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | docker run --rm -v "$GOPATH":/go -w /go/src/github.com/gliderlabs/logspout -e "GOPATH=/go" iron/go:dev sh -c 'go build -o logspout' 4 | 5 | # Can build the image too 6 | # docker build -t gliderlabs/logspout:latest . 7 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | apk add --update go build-base git mercurial ca-certificates 4 | cd /src 5 | go build -ldflags "-X main.Version=$1" -o /bin/logspout 6 | apk del go git mercurial build-base 7 | rm -rf /root/go /var/cache/apk/* 8 | 9 | # backwards compatibility 10 | ln -fs /tmp/docker.sock /var/run/docker.sock 11 | -------------------------------------------------------------------------------- /cfg/cfg.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import "os" 4 | 5 | // GetEnvDefault is a helper function to retrieve an env variable value OR return a default value 6 | func GetEnvDefault(name, dfault string) string { 7 | if val := os.Getenv(name); val != "" { 8 | return val 9 | } 10 | return dfault 11 | } 12 | -------------------------------------------------------------------------------- /custom/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gliderlabs/logspout:master 2 | ENV SYSLOG_FORMAT rfc3164 3 | -------------------------------------------------------------------------------- /custom/README.md: -------------------------------------------------------------------------------- 1 | # Custom Logspout Builds 2 | 3 | Forking logspout to change modules is unnecessary! Instead, you can create an 4 | empty Dockerfile based on `gliderlabs/logspout:master` and include a new 5 | `modules.go` file as well as the `build.sh` script that resides in the root of 6 | this repo for the build context that will override the standard one. 7 | 8 | This directory is an example of doing this. It pairs logspout down to just the 9 | syslog adapter and TCP transport. Note this means you can only create routes 10 | with `syslog+tcp` as the adapter. 11 | 12 | It also shows you can take this opportunity to change default configuration by 13 | setting environment in the Dockefile. Here we change the syslog adapter format 14 | from the default of `rfc5424` to old school `rfc3164`. 15 | 16 | Now you just have to `docker build` with this Dockerfile and you'll get a custom 17 | logspout container image. No need to install Go, no need to maintain a fork. 18 | -------------------------------------------------------------------------------- /custom/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | apk add --update go build-base git mercurial ca-certificates 4 | cd /src 5 | go build -ldflags "-X main.Version=$1" -o /bin/logspout 6 | apk del go git mercurial build-base 7 | rm -rf /root/go /var/cache/apk/* 8 | 9 | # backwards compatibility 10 | ln -fs /tmp/docker.sock /var/run/docker.sock 11 | -------------------------------------------------------------------------------- /custom/modules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/looplab/logspout-logstash" 5 | 6 | _ "github.com/gliderlabs/logspout/adapters/syslog" 7 | _ "github.com/gliderlabs/logspout/transports/tcp" 8 | _ "github.com/gliderlabs/logspout/transports/tls" 9 | _ "github.com/gliderlabs/logspout/transports/udp" 10 | ) 11 | -------------------------------------------------------------------------------- /debug/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu-debootstrap 2 | RUN apt-get update && apt-get install -y netcat 3 | -------------------------------------------------------------------------------- /debug/Makefile: -------------------------------------------------------------------------------- 1 | MODE ?= udp 2 | 3 | debug: 4 | @docker history logspout-debug &> /dev/null \ 5 | || docker build -f Dockerfile -t logspout-debug . 6 | ifeq ($(MODE), udp) 7 | @echo udp mode 8 | @docker run --rm -e LOGSPOUT=ignore --name logspout-debug logspout-debug nc -lvup 9000 9 | else 10 | @echo tcp mode 11 | @docker run --rm -e LOGSPOUT=ignore --name logspout-debug logspout-debug nc -lvp 9000 12 | endif 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gliderlabs/logspout 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Microsoft/hcsshim v0.8.14 // indirect 7 | github.com/Sirupsen/logrus v0.10.1-0.20160601113210-f3cfb454f4c2 // indirect 8 | github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68 // indirect 9 | github.com/containerd/containerd v1.4.3 // indirect 10 | github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 // indirect 11 | github.com/docker/docker v20.10.3+incompatible // indirect 12 | github.com/docker/engine-api v0.3.2-0.20160708123604-98348ad6f9c8 // indirect 13 | github.com/fsouza/go-dockerclient v1.7.0 14 | github.com/gogo/protobuf v1.3.2 // indirect 15 | github.com/gorilla/context v0.0.0-20160525203319-aed02d124ae4 // indirect 16 | github.com/gorilla/mux v1.8.0 17 | github.com/hashicorp/go-cleanhttp v0.0.0-20160407174126-ad28ea4487f0 // indirect 18 | github.com/hashicorp/golang-lru v0.5.4 // indirect 19 | github.com/looplab/logspout-logstash v0.0.0-20171130125839-68a4e47e757d 20 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect 21 | github.com/opencontainers/runc v1.0.0-rc1.0.20160706165155-9d7831e41d3e // indirect 22 | go.opencensus.io v0.22.6 // indirect 23 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b 24 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect 25 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 4 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= 7 | github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 8 | github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= 9 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 10 | github.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU= 11 | github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM= 12 | github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns= 13 | github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= 14 | github.com/Sirupsen/logrus v0.10.1-0.20160601113210-f3cfb454f4c2 h1:3BYvDlSNPyoYk6lr17s9IueNAabOBur3f3uVULjbhTA= 15 | github.com/Sirupsen/logrus v0.10.1-0.20160601113210-f3cfb454f4c2/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 16 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 17 | github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= 18 | github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 21 | github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk= 22 | github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= 23 | github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68 h1:hkGVFjz+plgr5UfxZUTPFbUFIF/Km6/s+RVRIRHLrrY= 24 | github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= 25 | github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= 26 | github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 27 | github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY= 28 | github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 29 | github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= 30 | github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 31 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 32 | github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a h1:jEIoR0aA5GogXZ8pP3DUzE+zrhaF6/1rYZy+7KkYEWM= 33 | github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY= 34 | github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 h1:6ejg6Lkk8dskcM7wQ28gONkukbQkM4qpj4RnYbpFzrI= 35 | github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= 36 | github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= 37 | github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= 38 | github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= 39 | github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= 40 | github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= 41 | github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= 42 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 43 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 44 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 45 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 46 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/docker/docker v1.4.2-0.20160708193732-ad969f1aa782 h1:akNYo0V5gmaDl8LUnd9G1/3Y8ohMl3s2b8gfvBHnURo= 49 | github.com/docker/docker v1.4.2-0.20160708193732-ad969f1aa782/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 50 | github.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible h1:lwpV3629md5omgAKjxPWX17shI7vMRpE3nyb9WHn8pA= 51 | github.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 52 | github.com/docker/docker v20.10.3+incompatible h1:+HS4XO73J41FpA260ztGujJ+0WibrA2TPJEnWNSyGNE= 53 | github.com/docker/docker v20.10.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 54 | github.com/docker/engine-api v0.3.2-0.20160708123604-98348ad6f9c8 h1:H443uS3liJKNFX8++m61ng44AnC+HKV1MU9Uhmq3hUk= 55 | github.com/docker/engine-api v0.3.2-0.20160708123604-98348ad6f9c8/go.mod h1:xtQCpzf4YysNZCVFfIGIm7qfLvYbxtLkEVVfKhTVOvw= 56 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 57 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 58 | github.com/docker/go-units v0.3.1 h1:QAFdsA6jLCnglbqE6mUsHuPcJlntY94DkxHf4deHKIU= 59 | github.com/docker/go-units v0.3.1/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 60 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 61 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 62 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 63 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 64 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 65 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 66 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 67 | github.com/fsouza/go-dockerclient v0.0.0-20160624230725-1a3d0cfd7814 h1:FSgKZZ2VFfEZkvrRfZ5LmUEgVapd9pnam49bjbV6a7M= 68 | github.com/fsouza/go-dockerclient v0.0.0-20160624230725-1a3d0cfd7814/go.mod h1:KpcjM623fQYE9MZiTGzKhjfxXAV9wbyX2C1cyRHfhl0= 69 | github.com/fsouza/go-dockerclient v1.7.0 h1:Ie1/8pAnBHNyCbSIDnYKBdXUEobk4AeJhWZz7k6rWfc= 70 | github.com/fsouza/go-dockerclient v1.7.0/go.mod h1:Ny0LfP7OOsYu9nAi4339E4Ifor6nGBFO2M8lnd2nR+c= 71 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 72 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 73 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 74 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 75 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 77 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 78 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 80 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 81 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 82 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 84 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 85 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 86 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 87 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 88 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 89 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 90 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 94 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 95 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 99 | github.com/gorilla/context v0.0.0-20160525203319-aed02d124ae4 h1:3nOfQt8sRPYbXORD5tJ8YyQ3HlL2Jt3LJ2U17CbNh6I= 100 | github.com/gorilla/context v0.0.0-20160525203319-aed02d124ae4/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 101 | github.com/gorilla/mux v0.0.0-20160605233521-9fa818a44c2b h1:OFvZV3a+25cGJH9dETHw0nk0wV6hLZI7IJijOkXEFS0= 102 | github.com/gorilla/mux v0.0.0-20160605233521-9fa818a44c2b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 103 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 104 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 105 | github.com/hashicorp/go-cleanhttp v0.0.0-20160407174126-ad28ea4487f0 h1:2l0haPDqCzZEO160UR5DSrrl8RWptFCoxFsSbRLJBaI= 106 | github.com/hashicorp/go-cleanhttp v0.0.0-20160407174126-ad28ea4487f0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 107 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 108 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 109 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 110 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 111 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 112 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 113 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 114 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 115 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 116 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 117 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 118 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 119 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 120 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 121 | github.com/looplab/logspout-logstash v0.0.0-20171130125839-68a4e47e757d h1:XHk6tU5oWCIrflIeYkS7/6nKMvqo3q//bJud+L4BguI= 122 | github.com/looplab/logspout-logstash v0.0.0-20171130125839-68a4e47e757d/go.mod h1:JGtIU22PbW89UiPrH/M1qcGZ9WkWlibloyUHGQ3Tgok= 123 | github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM= 124 | github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= 125 | github.com/moby/sys/mountinfo v0.4.0 h1:1KInV3Huv18akCu58V7lzNlt+jFmqlu1EaErnEHE/VM= 126 | github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= 127 | github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyPiK2mtZnPrpDl5UnZ64eCkw= 128 | github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= 129 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= 130 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= 131 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 132 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 133 | github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 134 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 135 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 136 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 137 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 138 | github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 139 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 140 | github.com/opencontainers/runc v1.0.0-rc1.0.20160706165155-9d7831e41d3e h1:SO9iqX0giNVXkTwPdEENwl1wK+RqviyAnCEbJS5azMU= 141 | github.com/opencontainers/runc v1.0.0-rc1.0.20160706165155-9d7831e41d3e/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 142 | github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 143 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 145 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 146 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 147 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 148 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 149 | github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 150 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 151 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 152 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 153 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 154 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 155 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 156 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 157 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 158 | github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 159 | github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 160 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 161 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 162 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 163 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 164 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 165 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 166 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 167 | github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 168 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 169 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 170 | go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= 171 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 172 | go.opencensus.io v0.22.6 h1:BdkrbWrzDlV9dnbzoP7sfN+dHheJ4J9JOaYxcUDL+ok= 173 | go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 174 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 175 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 176 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 177 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 178 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 179 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 180 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 181 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 182 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 183 | golang.org/x/net v0.0.0-20160707223729-f841c39de738 h1:N5K0l3yYkhlC0RSRoAhtspo2WgRvBMwZYoyB2ji+gkg= 184 | golang.org/x/net v0.0.0-20160707223729-f841c39de738/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 185 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 187 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 188 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 189 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 190 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 191 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 192 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 193 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 194 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 195 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 196 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 197 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 198 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 199 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 203 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 204 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 205 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= 207 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 208 | golang.org/x/sys v0.0.0-20160704031755-a408501be4d1 h1:QtO5ZFD1u7KisZhuRLL2GaZAZDdJqUcTdjFJj8OEePA= 209 | golang.org/x/sys v0.0.0-20160704031755-a408501be4d1/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 210 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 211 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 213 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20200922070232-aee5d888a860 h1:YEu4SMq7D0cmT7CBbXfcH0NZeuChAXwsHe/9XueUO6o= 224 | golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 226 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 228 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 230 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 231 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 232 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 233 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 234 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 235 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 236 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 237 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 238 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 239 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 240 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 241 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 242 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 243 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 244 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 246 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 248 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 249 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 250 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 251 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 252 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 253 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 254 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 255 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 256 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 257 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 258 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 259 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 260 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 261 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 262 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 263 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 264 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 265 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 266 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 267 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 268 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 269 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 270 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 271 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 272 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 273 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 274 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 275 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 276 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 277 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 278 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 279 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 280 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 281 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 282 | -------------------------------------------------------------------------------- /healthcheck/healthcheck.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | 8 | "github.com/gliderlabs/logspout/router" 9 | ) 10 | 11 | func init() { 12 | router.HTTPHandlers.Register(HealthCheck, "health") 13 | } 14 | 15 | // HealthCheck returns a http.Handler for the health check 16 | func HealthCheck() http.Handler { 17 | r := mux.NewRouter() 18 | r.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) { 19 | w.Write([]byte("Healthy!\n")) 20 | }) 21 | return r 22 | } 23 | -------------------------------------------------------------------------------- /httpstream/README.md: -------------------------------------------------------------------------------- 1 | # httpstream 2 | 3 | You can use these chunked transfer streaming endpoints for quick debugging with `curl` or for setting up easy TCP subscriptions to log sources. They also support WebSocket upgrades. 4 | 5 | GET /logs 6 | GET /logs/id: 7 | GET /logs/name: 8 | 9 | You can select specific log types from a source using a comma-delimited list in the query param `source`. Right now the only sources are `stdout` and `stderr`. 10 | 11 | If you include a request `Accept: application/json` header, the output will be JSON objects. Note that when upgrading to WebSocket, it will always use JSON. 12 | 13 | Since `/logs` and `/logs/name:` endpoints can return logs from multiple containers, they will by default return color-coded loglines prefixed with the name of the container. You can turn off the color escape codes with query param `colors=off` or the alternative is to stream the data in JSON format, which won't use colors or prefixes. 14 | 15 | -------------------------------------------------------------------------------- /httpstream/httpstream.go: -------------------------------------------------------------------------------- 1 | package httpstream 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/gorilla/mux" 12 | "golang.org/x/net/websocket" 13 | 14 | "github.com/gliderlabs/logspout/router" 15 | ) 16 | 17 | const maxRouteIDLen = 12 18 | 19 | func init() { 20 | router.HTTPHandlers.Register(LogStreamer, "logs") 21 | } 22 | 23 | func debug(v ...interface{}) { 24 | if os.Getenv("DEBUG") != "" { 25 | log.Println(v...) 26 | } 27 | } 28 | 29 | // LogStreamer returns a http.Handler that can stream logs 30 | func LogStreamer() http.Handler { 31 | logs := mux.NewRouter() 32 | logsHandler := func(w http.ResponseWriter, req *http.Request) { 33 | params := mux.Vars(req) 34 | route := new(router.Route) 35 | 36 | if params["value"] != "" { 37 | switch params["predicate"] { 38 | case "id": 39 | route.FilterID = params["value"] 40 | if len(route.ID) > maxRouteIDLen { 41 | route.FilterID = route.FilterID[:maxRouteIDLen] 42 | } 43 | case "name": 44 | route.FilterName = params["value"] 45 | } 46 | } 47 | 48 | if route.FilterID != "" && !router.Routes.RoutingFrom(route.FilterID) { 49 | http.NotFound(w, req) 50 | return 51 | } 52 | 53 | defer debug("http: logs streamer disconnected") 54 | logstream := make(chan *router.Message) 55 | defer close(logstream) 56 | 57 | var closer <-chan struct{} 58 | if req.Header.Get("Upgrade") == "websocket" { 59 | debug("http: logs streamer connected [websocket]") 60 | closerBi := make(chan struct{}) 61 | defer websocketStreamer(w, req, logstream, closerBi) 62 | closer = closerBi 63 | } else { 64 | debug("http: logs streamer connected [http]") 65 | defer httpStreamer(w, req, logstream, route.MultiContainer()) 66 | closer = req.Context().Done() 67 | } 68 | route.OverrideCloser(closer) 69 | 70 | router.Routes.Route(route, logstream) 71 | } 72 | logs.HandleFunc("/logs/{predicate:[a-zA-Z]+}:{value}", logsHandler).Methods("GET") 73 | logs.HandleFunc("/logs", logsHandler).Methods("GET") 74 | return logs 75 | } 76 | 77 | // Colorizer adds some color to the log stream 78 | type Colorizer map[string]int 79 | 80 | // Get returns up to 14 color escape codes (then repeats) for each unique key 81 | func (c Colorizer) Get(key string) string { 82 | i, exists := c[key] 83 | if !exists { 84 | c[key] = len(c) 85 | i = c[key] 86 | } 87 | bright := "1;" 88 | if i%14 > 6 { //nolint:gomnd 89 | bright = "" 90 | } 91 | return "\x1b[" + bright + "3" + strconv.Itoa(7-(i%7)) + "m" //nolint:gomnd 92 | } 93 | 94 | func marshal(obj interface{}) []byte { 95 | bytes, err := json.MarshalIndent(obj, "", " ") 96 | if err != nil { 97 | log.Println("marshal:", err) 98 | } 99 | return bytes 100 | } 101 | 102 | func normalName(name string) string { 103 | return name[1:] 104 | } 105 | 106 | func websocketStreamer(w http.ResponseWriter, req *http.Request, logstream chan *router.Message, closer chan struct{}) { 107 | websocket.Handler(func(conn *websocket.Conn) { 108 | for logline := range logstream { 109 | if req.URL.Query().Get("source") != "" && logline.Source != req.URL.Query().Get("source") { 110 | continue 111 | } 112 | _, err := conn.Write(append(marshal(logline), '\n')) 113 | if err != nil { 114 | closer <- struct{}{} 115 | return 116 | } 117 | } 118 | }).ServeHTTP(w, req) 119 | } 120 | 121 | func httpStreamer(w http.ResponseWriter, req *http.Request, logstream chan *router.Message, multi bool) { 122 | var colors Colorizer 123 | var usecolor, usejson bool 124 | nameWidth := 16 125 | if req.URL.Query().Get("colors") != "off" { 126 | colors = make(Colorizer) 127 | usecolor = true 128 | } 129 | if req.Header.Get("Accept") == "application/json" { 130 | w.Header().Add("Content-Type", "application/json") 131 | usejson = true 132 | } else { 133 | w.Header().Add("Content-Type", "text/plain") 134 | } 135 | for logline := range logstream { 136 | if req.URL.Query().Get("sources") != "" && logline.Source != req.URL.Query().Get("sources") { 137 | continue 138 | } 139 | if usejson { //nolint:nestif 140 | w.Write(append(marshal(logline), '\n')) 141 | } else { 142 | if multi { 143 | name := normalName(logline.Container.Name) 144 | if len(name) > nameWidth { 145 | nameWidth = len(name) 146 | } 147 | if usecolor { 148 | w.Write([]byte(fmt.Sprintf( 149 | "%s%"+strconv.Itoa(nameWidth)+"s|%s\x1b[0m\n", 150 | colors.Get(name), name, logline.Data, 151 | ))) 152 | } else { 153 | w.Write([]byte(fmt.Sprintf( 154 | "%"+strconv.Itoa(nameWidth)+"s|%s\n", name, logline.Data, 155 | ))) 156 | } 157 | } else { 158 | w.Write(append([]byte(logline.Data), '\n')) 159 | } 160 | } 161 | w.(http.Flusher).Flush() 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /logspout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | "github.com/gliderlabs/logspout/cfg" 11 | "github.com/gliderlabs/logspout/router" 12 | ) 13 | 14 | // Version is the running version of logspout 15 | var Version string 16 | 17 | func main() { 18 | if len(os.Args) == 2 && os.Args[1] == "--version" { 19 | fmt.Printf("%s\n", Version) 20 | os.Exit(0) 21 | } 22 | 23 | log.Printf("# logspout %s by gliderlabs\n", Version) 24 | log.Printf("# adapters: %s\n", strings.Join(router.AdapterFactories.Names(), " ")) 25 | log.Printf("# options : ") 26 | if d := cfg.GetEnvDefault("DEBUG", ""); d != "" { 27 | log.Printf("debug:%s\n", d) 28 | } 29 | if b := cfg.GetEnvDefault("BACKLOG", ""); b != "" { 30 | log.Printf("backlog:%s\n", b) 31 | } 32 | log.Printf("persist:%s\n", cfg.GetEnvDefault("ROUTESPATH", "/mnt/routes")) 33 | 34 | var jobs []string 35 | for _, job := range router.Jobs.All() { 36 | if err := job.Setup(); err != nil { 37 | log.Printf("!! %v\n", err) 38 | os.Exit(1) 39 | } 40 | if job.Name() != "" { 41 | jobs = append(jobs, job.Name()) 42 | } 43 | } 44 | log.Printf("# jobs : %s\n", strings.Join(jobs, " ")) 45 | 46 | routes, _ := router.Routes.GetAll() 47 | if len(routes) > 0 { 48 | log.Println("# routes :") 49 | w := new(tabwriter.Writer) 50 | w.Init(os.Stdout, 0, 8, 0, '\t', 0) 51 | fmt.Fprintln(w, "# ADAPTER\tADDRESS\tCONTAINERS\tSOURCES\tOPTIONS") //nolint:errcheck 52 | for _, route := range routes { 53 | fmt.Fprintf(w, "# %s\t%s\t%s\t%s\t%s\n", 54 | route.Adapter, 55 | route.Address, 56 | route.FilterID+route.FilterName+strings.Join(route.FilterLabels, ","), 57 | strings.Join(route.FilterSources, ","), 58 | route.Options) 59 | } 60 | w.Flush() 61 | } else { 62 | log.Println("# routes : none") 63 | } 64 | 65 | for _, job := range router.Jobs.All() { 66 | job := job 67 | go func() { 68 | log.Fatalf("%s ended: %s", job.Name(), job.Run()) 69 | }() 70 | } 71 | 72 | select {} 73 | } 74 | -------------------------------------------------------------------------------- /modules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/gliderlabs/logspout/adapters/multiline" 5 | _ "github.com/gliderlabs/logspout/adapters/raw" 6 | _ "github.com/gliderlabs/logspout/adapters/syslog" 7 | _ "github.com/gliderlabs/logspout/healthcheck" 8 | _ "github.com/gliderlabs/logspout/httpstream" 9 | _ "github.com/gliderlabs/logspout/routesapi" 10 | _ "github.com/gliderlabs/logspout/transports/tcp" 11 | _ "github.com/gliderlabs/logspout/transports/tls" 12 | _ "github.com/gliderlabs/logspout/transports/udp" 13 | ) 14 | -------------------------------------------------------------------------------- /router/extpoints.go: -------------------------------------------------------------------------------- 1 | // generated by go-extpoints -- DO NOT EDIT 2 | package router 3 | 4 | import ( 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | var registry = struct { 12 | sync.Mutex 13 | extpoints map[string]*extensionPoint 14 | }{ 15 | extpoints: make(map[string]*extensionPoint), 16 | } 17 | 18 | type extensionPoint struct { 19 | sync.Mutex 20 | iface reflect.Type 21 | components map[string]interface{} 22 | } 23 | 24 | func newExtensionPoint(iface interface{}) *extensionPoint { 25 | ep := &extensionPoint{ 26 | iface: reflect.TypeOf(iface).Elem(), 27 | components: make(map[string]interface{}), 28 | } 29 | registry.Lock() 30 | defer registry.Unlock() 31 | registry.extpoints[ep.iface.Name()] = ep 32 | return ep 33 | } 34 | 35 | func (ep *extensionPoint) lookup(name string) (ext interface{}, ok bool) { 36 | ep.Lock() 37 | defer ep.Unlock() 38 | ext, ok = ep.components[name] 39 | return 40 | } 41 | 42 | func (ep *extensionPoint) all() map[string]interface{} { 43 | ep.Lock() 44 | defer ep.Unlock() 45 | all := make(map[string]interface{}) 46 | for k, v := range ep.components { 47 | all[k] = v 48 | } 49 | return all 50 | } 51 | 52 | func (ep *extensionPoint) register(component interface{}, name string) bool { 53 | ep.Lock() 54 | defer ep.Unlock() 55 | if name == "" { 56 | comType := reflect.TypeOf(component) 57 | if comType.Kind() == reflect.Func { 58 | nameParts := strings.Split(runtime.FuncForPC( 59 | reflect.ValueOf(component).Pointer()).Name(), ".") 60 | name = nameParts[len(nameParts)-1] 61 | } else { 62 | name = comType.Elem().Name() 63 | } 64 | } 65 | _, exists := ep.components[name] 66 | if exists { 67 | return false 68 | } 69 | ep.components[name] = component 70 | return true 71 | } 72 | 73 | func (ep *extensionPoint) unregister(name string) bool { 74 | ep.Lock() 75 | defer ep.Unlock() 76 | _, exists := ep.components[name] 77 | if !exists { 78 | return false 79 | } 80 | delete(ep.components, name) 81 | return true 82 | } 83 | 84 | func implements(component interface{}) []string { 85 | var ifaces []string 86 | typ := reflect.TypeOf(component) 87 | for name, ep := range registry.extpoints { 88 | if ep.iface.Kind() == reflect.Func && typ.AssignableTo(ep.iface) { 89 | ifaces = append(ifaces, name) 90 | } 91 | if ep.iface.Kind() != reflect.Func && typ.Implements(ep.iface) { 92 | ifaces = append(ifaces, name) 93 | } 94 | } 95 | return ifaces 96 | } 97 | 98 | func Register(component interface{}, name string) []string { 99 | registry.Lock() 100 | defer registry.Unlock() 101 | var ifaces []string 102 | for _, iface := range implements(component) { 103 | if ok := registry.extpoints[iface].register(component, name); ok { 104 | ifaces = append(ifaces, iface) 105 | } 106 | } 107 | return ifaces 108 | } 109 | 110 | func Unregister(name string) []string { 111 | registry.Lock() 112 | defer registry.Unlock() 113 | var ifaces []string 114 | for iface, extpoint := range registry.extpoints { 115 | if ok := extpoint.unregister(name); ok { 116 | ifaces = append(ifaces, iface) 117 | } 118 | } 119 | return ifaces 120 | } 121 | 122 | // HTTPHandler 123 | 124 | var HTTPHandlers = &httpHandlerExt{ 125 | newExtensionPoint(new(HTTPHandler)), 126 | } 127 | 128 | type httpHandlerExt struct { 129 | *extensionPoint 130 | } 131 | 132 | func (ep *httpHandlerExt) Unregister(name string) bool { 133 | return ep.unregister(name) 134 | } 135 | 136 | func (ep *httpHandlerExt) Register(component HTTPHandler, name string) bool { 137 | return ep.register(component, name) 138 | } 139 | 140 | func (ep *httpHandlerExt) Lookup(name string) (HTTPHandler, bool) { 141 | ext, ok := ep.lookup(name) 142 | if !ok { 143 | return nil, ok 144 | } 145 | return ext.(HTTPHandler), ok 146 | } 147 | 148 | func (ep *httpHandlerExt) All() map[string]HTTPHandler { 149 | all := make(map[string]HTTPHandler) 150 | for k, v := range ep.all() { 151 | all[k] = v.(HTTPHandler) 152 | } 153 | return all 154 | } 155 | 156 | func (ep *httpHandlerExt) Names() []string { 157 | var names []string 158 | for k := range ep.all() { 159 | names = append(names, k) 160 | } 161 | return names 162 | } 163 | 164 | // AdapterFactory 165 | 166 | var AdapterFactories = &adapterFactoryExt{ 167 | newExtensionPoint(new(AdapterFactory)), 168 | } 169 | 170 | type adapterFactoryExt struct { 171 | *extensionPoint 172 | } 173 | 174 | func (ep *adapterFactoryExt) Unregister(name string) bool { 175 | return ep.unregister(name) 176 | } 177 | 178 | func (ep *adapterFactoryExt) Register(component AdapterFactory, name string) bool { 179 | return ep.register(component, name) 180 | } 181 | 182 | func (ep *adapterFactoryExt) Lookup(name string) (AdapterFactory, bool) { 183 | ext, ok := ep.lookup(name) 184 | if !ok { 185 | return nil, ok 186 | } 187 | return ext.(AdapterFactory), ok 188 | } 189 | 190 | func (ep *adapterFactoryExt) All() map[string]AdapterFactory { 191 | all := make(map[string]AdapterFactory) 192 | for k, v := range ep.all() { 193 | all[k] = v.(AdapterFactory) 194 | } 195 | return all 196 | } 197 | 198 | func (ep *adapterFactoryExt) Names() []string { 199 | var names []string 200 | for k := range ep.all() { 201 | names = append(names, k) 202 | } 203 | return names 204 | } 205 | 206 | // AdapterTransport 207 | 208 | var AdapterTransports = &adapterTransportExt{ 209 | newExtensionPoint(new(AdapterTransport)), 210 | } 211 | 212 | type adapterTransportExt struct { 213 | *extensionPoint 214 | } 215 | 216 | func (ep *adapterTransportExt) Unregister(name string) bool { 217 | return ep.unregister(name) 218 | } 219 | 220 | func (ep *adapterTransportExt) Register(component AdapterTransport, name string) bool { 221 | return ep.register(component, name) 222 | } 223 | 224 | func (ep *adapterTransportExt) Lookup(name string) (AdapterTransport, bool) { 225 | ext, ok := ep.lookup(name) 226 | if !ok { 227 | return nil, ok 228 | } 229 | return ext.(AdapterTransport), ok 230 | } 231 | 232 | func (ep *adapterTransportExt) All() map[string]AdapterTransport { 233 | all := make(map[string]AdapterTransport) 234 | for k, v := range ep.all() { 235 | all[k] = v.(AdapterTransport) 236 | } 237 | return all 238 | } 239 | 240 | func (ep *adapterTransportExt) Names() []string { 241 | var names []string 242 | for k := range ep.all() { 243 | names = append(names, k) 244 | } 245 | return names 246 | } 247 | 248 | // Job 249 | 250 | var Jobs = &jobExt{ 251 | newExtensionPoint(new(Job)), 252 | } 253 | 254 | type jobExt struct { 255 | *extensionPoint 256 | } 257 | 258 | func (ep *jobExt) Unregister(name string) bool { 259 | return ep.unregister(name) 260 | } 261 | 262 | func (ep *jobExt) Register(component Job, name string) bool { 263 | return ep.register(component, name) 264 | } 265 | 266 | func (ep *jobExt) Lookup(name string) (Job, bool) { 267 | ext, ok := ep.lookup(name) 268 | if !ok { 269 | return nil, ok 270 | } 271 | return ext.(Job), ok 272 | } 273 | 274 | func (ep *jobExt) All() map[string]Job { 275 | all := make(map[string]Job) 276 | for k, v := range ep.all() { 277 | all[k] = v.(Job) 278 | } 279 | return all 280 | } 281 | 282 | func (ep *jobExt) Names() []string { 283 | var names []string 284 | for k := range ep.all() { 285 | names = append(names, k) 286 | } 287 | return names 288 | } 289 | 290 | // LogRouter 291 | 292 | var LogRouters = &logRouterExt{ 293 | newExtensionPoint(new(LogRouter)), 294 | } 295 | 296 | type logRouterExt struct { 297 | *extensionPoint 298 | } 299 | 300 | func (ep *logRouterExt) Unregister(name string) bool { 301 | return ep.unregister(name) 302 | } 303 | 304 | func (ep *logRouterExt) Register(component LogRouter, name string) bool { 305 | return ep.register(component, name) 306 | } 307 | 308 | func (ep *logRouterExt) Lookup(name string) (LogRouter, bool) { 309 | ext, ok := ep.lookup(name) 310 | if !ok { 311 | return nil, ok 312 | } 313 | return ext.(LogRouter), ok 314 | } 315 | 316 | func (ep *logRouterExt) All() map[string]LogRouter { 317 | all := make(map[string]LogRouter) 318 | for k, v := range ep.all() { 319 | all[k] = v.(LogRouter) 320 | } 321 | return all 322 | } 323 | 324 | func (ep *logRouterExt) Names() []string { 325 | var names []string 326 | for k := range ep.all() { 327 | names = append(names, k) 328 | } 329 | return names 330 | } 331 | -------------------------------------------------------------------------------- /router/http.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gliderlabs/logspout/cfg" 9 | ) 10 | 11 | func init() { 12 | bindAddress := cfg.GetEnvDefault("HTTP_BIND_ADDRESS", "0.0.0.0") 13 | port := cfg.GetEnvDefault("PORT", cfg.GetEnvDefault("HTTP_PORT", "80")) 14 | Jobs.Register(&httpService{bindAddress, port}, "http") 15 | } 16 | 17 | type httpService struct { 18 | bindAddress string 19 | port string 20 | } 21 | 22 | func (s *httpService) Name() string { 23 | return fmt.Sprintf("http[%s]:%s", 24 | strings.Join(HTTPHandlers.Names(), ","), s.port) 25 | } 26 | 27 | func (s *httpService) Setup() error { 28 | for name, handler := range HTTPHandlers.All() { 29 | h := handler() 30 | http.Handle("/"+name, h) 31 | http.Handle("/"+name+"/", h) 32 | } 33 | return nil 34 | } 35 | 36 | func (s *httpService) Run() error { 37 | return http.ListenAndServe(s.bindAddress+":"+s.port, nil) 38 | } 39 | -------------------------------------------------------------------------------- /router/persist.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // RouteFileStore represents a directory for storing routes 13 | type RouteFileStore string 14 | 15 | // Filename returns the filename in a RouteFileStore for a given id 16 | func (fs RouteFileStore) Filename(id string) string { 17 | return string(fs) + "/" + id + ".json" 18 | } 19 | 20 | // Get returns *Route based on an id 21 | func (fs RouteFileStore) Get(id string) (*Route, error) { 22 | file, err := os.Open(fs.Filename(id)) 23 | if err != nil { 24 | return nil, err 25 | } 26 | route := new(Route) 27 | if err = unmarshal(file, route); err != nil { 28 | return nil, err 29 | } 30 | return route, nil 31 | } 32 | 33 | // GetAll returns a slice of *Route for the entire RouteFileStore 34 | func (fs RouteFileStore) GetAll() ([]*Route, error) { 35 | files, err := ioutil.ReadDir(string(fs)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | var routes []*Route 40 | for _, file := range files { 41 | fileparts := strings.Split(file.Name(), ".") 42 | if len(fileparts) > 1 && fileparts[1] == "json" { 43 | route, err := fs.Get(fileparts[0]) 44 | if err == nil { 45 | routes = append(routes, route) 46 | } 47 | } 48 | } 49 | return routes, nil 50 | } 51 | 52 | // Add writes a marshaled *Route to the RouteFileStore 53 | func (fs RouteFileStore) Add(route *Route) error { 54 | return ioutil.WriteFile(fs.Filename(route.ID), marshal(route), 0600) 55 | } 56 | 57 | // Remove removes route from the RouteFileStore based on id 58 | func (fs RouteFileStore) Remove(id string) bool { 59 | if _, err := os.Stat(fs.Filename(id)); err == nil { 60 | if err := os.Remove(fs.Filename(id)); err != nil { 61 | return true 62 | } 63 | } 64 | return false 65 | } 66 | 67 | func marshal(obj interface{}) []byte { 68 | bytes, err := json.MarshalIndent(obj, "", " ") 69 | if err != nil { 70 | log.Println("marshal:", err) 71 | } 72 | return bytes 73 | } 74 | 75 | func unmarshal(input io.Reader, obj interface{}) error { 76 | dec := json.NewDecoder(input) 77 | return dec.Decode(obj) 78 | } 79 | -------------------------------------------------------------------------------- /router/pump.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | docker "github.com/fsouza/go-dockerclient" 14 | 15 | "github.com/gliderlabs/logspout/cfg" 16 | ) 17 | 18 | const ( 19 | defaultPumpName = "pump" 20 | pumpEventStatusStartName = "start" 21 | pumpEventStatusRestartName = "restart" 22 | pumpEventStatusRenameName = "rename" 23 | pumpEventStatusDieName = "die" 24 | trueString = "true" 25 | pumpMaxIDLen = 12 26 | ) 27 | 28 | var ( 29 | allowTTY bool 30 | ) 31 | 32 | func init() { 33 | pump := &LogsPump{ 34 | pumps: make(map[string]*containerPump), 35 | routes: make(map[chan *update]struct{}), 36 | } 37 | setAllowTTY() 38 | LogRouters.Register(pump, defaultPumpName) 39 | Jobs.Register(pump, defaultPumpName) 40 | } 41 | 42 | func debug(v ...interface{}) { 43 | if os.Getenv("DEBUG") != "" { 44 | log.Println(v...) 45 | } 46 | } 47 | 48 | func backlog() bool { 49 | return os.Getenv("BACKLOG") == trueString 50 | } 51 | 52 | func setAllowTTY() { 53 | if t := cfg.GetEnvDefault("ALLOW_TTY", ""); t == trueString { 54 | allowTTY = true 55 | } 56 | debug("setting allowTTY to:", allowTTY) 57 | } 58 | 59 | func assert(err error, context string) { 60 | if err != nil { 61 | log.Fatal(context+": ", err) 62 | } 63 | } 64 | 65 | func normalName(name string) string { 66 | return name[1:] 67 | } 68 | 69 | func normalID(id string) string { 70 | if len(id) > pumpMaxIDLen { 71 | return id[:pumpMaxIDLen] 72 | } 73 | return id 74 | } 75 | 76 | func logDriverSupported(container *docker.Container) bool { 77 | switch container.HostConfig.LogConfig.Type { 78 | case "json-file", "journald", "db": 79 | return true 80 | default: 81 | return false 82 | } 83 | } 84 | 85 | func ignoreContainer(container *docker.Container) bool { 86 | for _, kv := range container.Config.Env { 87 | kvp := strings.SplitN(kv, "=", 2) 88 | if len(kvp) == 2 && kvp[0] == "LOGSPOUT" && strings.EqualFold(kvp[1], "ignore") { 89 | return true 90 | } 91 | } 92 | 93 | excludeLabel := cfg.GetEnvDefault("EXCLUDE_LABELS", "") 94 | 95 | if excludeLabel == "" { 96 | excludeLabel = cfg.GetEnvDefault("EXCLUDE_LABEL", "") 97 | } 98 | excludeValue := "true" 99 | // support EXCLUDE_LABEL having multiple custom label values 100 | excludeLabelArr := strings.Split(excludeLabel, ";") 101 | 102 | for _, label := range excludeLabelArr { 103 | labelParts := strings.Split(label, ":") 104 | 105 | if len(labelParts) == 2 { //nolint:gomnd 106 | excludeLabel = labelParts[0] 107 | excludeValue = labelParts[1] 108 | } 109 | 110 | if value, ok := container.Config.Labels[excludeLabel]; ok { 111 | if len(excludeLabel) > 0 && strings.EqualFold(value, strings.ToLower(excludeValue)) { 112 | return true 113 | } 114 | } 115 | } 116 | return false 117 | } 118 | 119 | func ignoreContainerTTY(container *docker.Container) bool { 120 | if container.Config.Tty && !allowTTY { 121 | return true 122 | } 123 | return false 124 | } 125 | 126 | func getInactivityTimeoutFromEnv() time.Duration { 127 | inactivityTimeout, err := time.ParseDuration(cfg.GetEnvDefault("INACTIVITY_TIMEOUT", "0")) 128 | assert(err, "Couldn't parse env var INACTIVITY_TIMEOUT. See https://golang.org/pkg/time/#ParseDuration for valid format.") 129 | return inactivityTimeout 130 | } 131 | 132 | type update struct { 133 | *docker.APIEvents 134 | pump *containerPump 135 | } 136 | 137 | // LogsPump is responsible for "pumping" logs to their configured destinations 138 | type LogsPump struct { 139 | mu sync.Mutex 140 | pumps map[string]*containerPump 141 | routes map[chan *update]struct{} 142 | client *docker.Client 143 | } 144 | 145 | // Name returns the name of the pump 146 | func (p *LogsPump) Name() string { 147 | return defaultPumpName 148 | } 149 | 150 | // Setup configures the pump 151 | func (p *LogsPump) Setup() error { 152 | var err error 153 | p.client, err = docker.NewClientFromEnv() 154 | return err 155 | } 156 | 157 | func (p *LogsPump) rename(event *docker.APIEvents) { 158 | p.mu.Lock() 159 | defer p.mu.Unlock() 160 | container, err := p.client.InspectContainerWithOptions(docker.InspectContainerOptions{ID: event.ID}) 161 | assert(err, defaultPumpName) 162 | pump, ok := p.pumps[normalID(event.ID)] 163 | if !ok { 164 | debug("pump.rename(): ignore: pump not found, state:", container.State.StateString()) 165 | return 166 | } 167 | pump.container.Name = container.Name 168 | } 169 | 170 | // Run executes the pump 171 | func (p *LogsPump) Run() error { 172 | inactivityTimeout := getInactivityTimeoutFromEnv() 173 | debug("pump.Run(): using inactivity timeout: ", inactivityTimeout) 174 | 175 | containers, err := p.client.ListContainers(docker.ListContainersOptions{}) 176 | if err != nil { 177 | return err 178 | } 179 | for idx := range containers { 180 | p.pumpLogs(&docker.APIEvents{ 181 | ID: normalID(containers[idx].ID), 182 | Status: pumpEventStatusStartName, 183 | }, backlog(), inactivityTimeout) 184 | } 185 | events := make(chan *docker.APIEvents) 186 | err = p.client.AddEventListener(events) 187 | if err != nil { 188 | return err 189 | } 190 | for event := range events { 191 | debug("pump.Run() event:", normalID(event.ID), event.Status) 192 | switch event.Status { 193 | case pumpEventStatusStartName, pumpEventStatusRestartName: 194 | go p.pumpLogs(event, backlog(), inactivityTimeout) 195 | case pumpEventStatusRenameName: 196 | go p.rename(event) 197 | case pumpEventStatusDieName: 198 | go p.update(event) 199 | } 200 | } 201 | return errors.New("docker event stream closed") 202 | } 203 | 204 | func (p *LogsPump) pumpLogs(event *docker.APIEvents, backlog bool, inactivityTimeout time.Duration) { //nolint:gocyclo 205 | id := normalID(event.ID) 206 | container, err := p.client.InspectContainerWithOptions(docker.InspectContainerOptions{ID: id}) 207 | assert(err, defaultPumpName) 208 | if ignoreContainerTTY(container) { 209 | debug("pump.pumpLogs():", id, "ignored: tty enabled") 210 | return 211 | } 212 | if ignoreContainer(container) { 213 | debug("pump.pumpLogs():", id, "ignored: environ ignore") 214 | return 215 | } 216 | if !logDriverSupported(container) { 217 | debug("pump.pumpLogs():", id, "ignored: log driver not supported") 218 | return 219 | } 220 | 221 | var tail = cfg.GetEnvDefault("TAIL", "all") 222 | var sinceTime time.Time 223 | if backlog { 224 | sinceTime = time.Unix(0, 0) 225 | } else { 226 | sinceTime = time.Now() 227 | } 228 | 229 | p.mu.Lock() 230 | if _, exists := p.pumps[id]; exists { 231 | p.mu.Unlock() 232 | debug("pump.pumpLogs():", id, "pump exists") 233 | return 234 | } 235 | 236 | // RawTerminal with container Tty=false injects binary headers into 237 | // the log stream that show up as garbage unicode characters 238 | rawTerminal := false 239 | if allowTTY && container.Config.Tty { 240 | rawTerminal = true 241 | } 242 | outrd, outwr := io.Pipe() 243 | errrd, errwr := io.Pipe() 244 | p.pumps[id] = newContainerPump(container, outrd, errrd) 245 | p.mu.Unlock() 246 | p.update(event) 247 | go func() { 248 | for { 249 | debug("pump.pumpLogs():", id, "started, tail:", tail) 250 | err := p.client.Logs(docker.LogsOptions{ 251 | Container: id, 252 | OutputStream: outwr, 253 | ErrorStream: errwr, 254 | Stdout: true, 255 | Stderr: true, 256 | Follow: true, 257 | Tail: tail, 258 | Since: sinceTime.Unix(), 259 | InactivityTimeout: inactivityTimeout, 260 | RawTerminal: rawTerminal, 261 | }) 262 | if err != nil { 263 | debug("pump.pumpLogs():", id, "stopped with error:", err) 264 | } else { 265 | debug("pump.pumpLogs():", id, "stopped") 266 | } 267 | 268 | sinceTime = time.Now() 269 | if err == docker.ErrInactivityTimeout { 270 | sinceTime = sinceTime.Add(-inactivityTimeout) 271 | } 272 | 273 | container, err := p.client.InspectContainerWithOptions(docker.InspectContainerOptions{ID: id}) 274 | if err != nil { 275 | _, four04 := err.(*docker.NoSuchContainer) 276 | if !four04 { 277 | assert(err, defaultPumpName) 278 | } 279 | } else if container.State.Running { 280 | continue 281 | } 282 | 283 | debug("pump.pumpLogs():", id, "dead") 284 | outwr.Close() 285 | errwr.Close() 286 | p.mu.Lock() 287 | delete(p.pumps, id) 288 | p.mu.Unlock() 289 | return 290 | } 291 | }() 292 | } 293 | 294 | func (p *LogsPump) update(event *docker.APIEvents) { 295 | p.mu.Lock() 296 | defer p.mu.Unlock() 297 | pump, pumping := p.pumps[normalID(event.ID)] 298 | if pumping { 299 | for r := range p.routes { 300 | select { 301 | case r <- &update{event, pump}: 302 | case <-time.After(time.Second * 1): 303 | debug("pump.update(): route timeout, dropping") 304 | defer delete(p.routes, r) 305 | } 306 | } 307 | } 308 | } 309 | 310 | // RoutingFrom returns whether a container id is routing from this pump 311 | func (p *LogsPump) RoutingFrom(id string) bool { 312 | p.mu.Lock() 313 | defer p.mu.Unlock() 314 | _, monitoring := p.pumps[normalID(id)] 315 | return monitoring 316 | } 317 | 318 | // Route takes a logstream and routes it according to the supplied Route 319 | func (p *LogsPump) Route(route *Route, logstream chan *Message) { 320 | p.mu.Lock() 321 | for _, pump := range p.pumps { 322 | if route.MatchContainer( 323 | normalID(pump.container.ID), 324 | normalName(pump.container.Name), 325 | pump.container.Config.Labels, 326 | ) { 327 | pump.add(logstream, route) 328 | defer pump.remove(logstream) 329 | } 330 | } 331 | updates := make(chan *update) 332 | p.routes[updates] = struct{}{} 333 | p.mu.Unlock() 334 | defer func() { 335 | p.mu.Lock() 336 | delete(p.routes, updates) 337 | p.mu.Unlock() 338 | route.closed = true 339 | }() 340 | for { 341 | select { 342 | case event := <-updates: 343 | switch event.Status { 344 | case pumpEventStatusStartName, pumpEventStatusRestartName: 345 | if route.MatchContainer( 346 | normalID(event.pump.container.ID), 347 | normalName(event.pump.container.Name), 348 | event.pump.container.Config.Labels, 349 | ) { 350 | event.pump.add(logstream, route) 351 | defer event.pump.remove(logstream) 352 | } 353 | case pumpEventStatusDieName: 354 | if strings.HasPrefix(route.FilterID, event.ID) { 355 | // If the route is just about a single container, 356 | // we can stop routing when it dies. 357 | return 358 | } 359 | } 360 | case <-route.Closer(): 361 | return 362 | } 363 | } 364 | } 365 | 366 | type containerPump struct { 367 | sync.Mutex 368 | container *docker.Container 369 | logstreams map[chan *Message]*Route 370 | } 371 | 372 | func newContainerPump(container *docker.Container, stdout, stderr io.Reader) *containerPump { 373 | cp := &containerPump{ 374 | container: container, 375 | logstreams: make(map[chan *Message]*Route), 376 | } 377 | pump := func(source string, input io.Reader) { 378 | buf := bufio.NewReader(input) 379 | for { 380 | line, err := buf.ReadString('\n') 381 | if err != nil { 382 | if err != io.EOF { 383 | debug("pump.newContainerPump():", normalID(container.ID), source+":", err) 384 | } 385 | return 386 | } 387 | cp.send(&Message{ 388 | Data: strings.TrimSuffix(line, "\n"), 389 | Container: container, 390 | Time: time.Now(), 391 | Source: source, 392 | }) 393 | } 394 | } 395 | go pump("stdout", stdout) 396 | go pump("stderr", stderr) 397 | return cp 398 | } 399 | 400 | func (cp *containerPump) send(msg *Message) { 401 | cp.Lock() 402 | defer cp.Unlock() 403 | for logstream, route := range cp.logstreams { 404 | if !route.MatchMessage(msg) { 405 | continue 406 | } 407 | logstream <- msg 408 | } 409 | } 410 | 411 | func (cp *containerPump) add(logstream chan *Message, route *Route) { 412 | cp.Lock() 413 | defer cp.Unlock() 414 | cp.logstreams[logstream] = route 415 | } 416 | 417 | func (cp *containerPump) remove(logstream chan *Message) { 418 | cp.Lock() 419 | defer cp.Unlock() 420 | delete(cp.logstreams, logstream) 421 | } 422 | -------------------------------------------------------------------------------- /router/pump_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "testing" 12 | 13 | docker "github.com/fsouza/go-dockerclient" 14 | ) 15 | 16 | type FakeRoundTripper struct { 17 | message interface{} 18 | status int 19 | header map[string]string 20 | requests []*http.Request 21 | } 22 | 23 | func (rt *FakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 24 | b, err := json.Marshal(rt.message) 25 | if err != nil { 26 | log.Fatal(err.Error()) 27 | } 28 | 29 | body := bytes.NewReader(b) 30 | rt.requests = append(rt.requests, r) 31 | res := &http.Response{ 32 | StatusCode: rt.status, 33 | Body: ioutil.NopCloser(body), 34 | Header: make(http.Header), 35 | } 36 | for k, v := range rt.header { 37 | res.Header.Set(k, v) 38 | } 39 | return res, nil 40 | } 41 | 42 | func (rt *FakeRoundTripper) Reset() { 43 | rt.requests = nil 44 | } 45 | 46 | func newTestClient(rt http.RoundTripper) docker.Client { 47 | endpoint := "http://localhost:4243" 48 | client, _ := docker.NewClient(endpoint) 49 | client.HTTPClient = &http.Client{Transport: rt} 50 | client.Dialer = &net.Dialer{} 51 | client.SkipServerVersionCheck = true 52 | return *client 53 | } 54 | 55 | func TestPumpIgnoreContainer(t *testing.T) { 56 | os.Setenv("EXCLUDE_LABEL", "exclude") 57 | defer os.Unsetenv("EXCLUDE_LABEL") 58 | containers := []struct { 59 | in *docker.Config 60 | out bool 61 | }{ 62 | {&docker.Config{Env: []string{"foo", "bar"}}, false}, 63 | {&docker.Config{Env: []string{"LOGSPOUT=ignore"}}, true}, 64 | {&docker.Config{Env: []string{"LOGSPOUT=IGNORE"}}, true}, 65 | {&docker.Config{Env: []string{"LOGSPOUT=foo"}}, false}, 66 | {&docker.Config{Labels: map[string]string{"exclude": "true"}}, true}, 67 | {&docker.Config{Labels: map[string]string{"exclude": "false"}}, false}, 68 | } 69 | 70 | for _, conf := range containers { 71 | if actual := ignoreContainer(&docker.Container{Config: conf.in}); actual != conf.out { 72 | t.Errorf("expected %v got %v", conf.out, actual) 73 | } 74 | } 75 | } 76 | 77 | func TestPumpIgnoreContainerCustomLabels(t *testing.T) { 78 | os.Setenv("EXCLUDE_LABEL", "k8s-app:canal") 79 | defer os.Unsetenv("EXCLUDE_LABEL") 80 | containers := []struct { 81 | in *docker.Config 82 | out bool 83 | }{ 84 | {&docker.Config{Labels: map[string]string{"k8s-app": "canal"}}, true}, 85 | {&docker.Config{Labels: map[string]string{"app": "demo-app"}}, false}, 86 | } 87 | 88 | for _, conf := range containers { 89 | if actual := ignoreContainer(&docker.Container{Config: conf.in}); actual != conf.out { 90 | t.Errorf("expected %v got %v", conf.out, actual) 91 | } 92 | } 93 | } 94 | 95 | func TestPumpIgnoreContainersMatchingAtLeastOneLabel(t *testing.T) { 96 | os.Setenv("EXCLUDE_LABEL", "k8s-app:canal;io.kubernetes.pod.namespace:default") 97 | defer os.Unsetenv("EXCLUDE_LABEL") 98 | containers := []struct { 99 | in *docker.Config 100 | out bool 101 | }{ 102 | {&docker.Config{Labels: map[string]string{"k8s-app": "canal"}}, true}, 103 | {&docker.Config{Labels: map[string]string{"app": "demo-app"}}, false}, 104 | {&docker.Config{Labels: map[string]string{"io.kubernetes.pod.namespace": "kube-system"}}, false}, 105 | {&docker.Config{Labels: map[string]string{"io.kubernetes.pod.namespace": "default"}}, true}, 106 | } 107 | 108 | for _, conf := range containers { 109 | if actual := ignoreContainer(&docker.Container{Config: conf.in}); actual != conf.out { 110 | t.Errorf("expected %v got %v", conf.out, actual) 111 | } 112 | } 113 | } 114 | 115 | func TestPumpIgnoreContainerCustomLabelsUsingExcludeLabelsEnvVar(t *testing.T) { 116 | os.Setenv("EXCLUDE_LABELS", "k8s-app:canal;io.kubernetes.pod.namespace:production") 117 | defer os.Unsetenv("EXCLUDE_LABELS") 118 | containers := []struct { 119 | in *docker.Config 120 | out bool 121 | }{ 122 | {&docker.Config{Labels: map[string]string{"k8s-app": "canal"}}, true}, 123 | {&docker.Config{Labels: map[string]string{"app": "demo-app"}}, false}, 124 | {&docker.Config{Labels: map[string]string{"io.kubernetes.pod.namespace": "production"}}, true}, 125 | } 126 | 127 | for _, conf := range containers { 128 | if actual := ignoreContainer(&docker.Container{Config: conf.in}); actual != conf.out { 129 | t.Errorf("expected %v got %v", conf.out, actual) 130 | } 131 | } 132 | } 133 | 134 | func TestPumpIgnoreContainerAllowTTYDefault(t *testing.T) { 135 | containers := []struct { 136 | in *docker.Config 137 | out bool 138 | }{ 139 | {&docker.Config{Tty: true}, true}, 140 | {&docker.Config{Tty: false}, false}, 141 | } 142 | 143 | for _, conf := range containers { 144 | if actual := ignoreContainerTTY(&docker.Container{Config: conf.in}); actual != conf.out { 145 | t.Errorf("expected %v got %v", conf.out, actual) 146 | } 147 | } 148 | } 149 | 150 | func TestPumpIgnoreContainerAllowTTYTrue(t *testing.T) { 151 | os.Setenv("ALLOW_TTY", "true") 152 | defer os.Unsetenv("ALLOW_TTY") 153 | 154 | setAllowTTY() 155 | containers := []struct { 156 | in *docker.Config 157 | out bool 158 | }{ 159 | {&docker.Config{Tty: true}, false}, 160 | {&docker.Config{Tty: false}, false}, 161 | } 162 | for _, conf := range containers { 163 | if actual := ignoreContainerTTY(&docker.Container{Config: conf.in}); actual != conf.out { 164 | t.Errorf("expected %v got %v", conf.out, actual) 165 | } 166 | } 167 | } 168 | 169 | func TestPumpLogsPumpName(t *testing.T) { 170 | p := &LogsPump{} 171 | if name := p.Name(); name != "pump" { 172 | t.Error("name should be 'pump' got:", name) 173 | } 174 | } 175 | 176 | func TestPumpContainerRename(t *testing.T) { 177 | container := &docker.Container{ 178 | ID: "8dfafdbc3a40", 179 | Name: "bar", 180 | } 181 | client := newTestClient(&FakeRoundTripper{message: container, status: http.StatusOK}) 182 | p := &LogsPump{ 183 | client: &client, 184 | pumps: make(map[string]*containerPump), 185 | routes: make(map[chan *update]struct{}), 186 | } 187 | config := &docker.Config{ 188 | Tty: false, 189 | } 190 | container = &docker.Container{ 191 | ID: "8dfafdbc3a40", 192 | Name: "foo", 193 | Config: config, 194 | } 195 | p.pumps["8dfafdbc3a40"] = newContainerPump(container, os.Stdout, os.Stderr) 196 | if name := p.pumps["8dfafdbc3a40"].container.Name; name != "foo" { 197 | t.Errorf("containerPump should have name: 'foo' got name: '%s'", name) 198 | } 199 | p.rename(&docker.APIEvents{ID: "8dfafdbc3a40"}) 200 | if name := p.pumps["8dfafdbc3a40"].container.Name; name != "bar" { 201 | t.Errorf("containerPump should have name: 'bar' got name: %s", name) 202 | } 203 | } 204 | 205 | func TestPumpNewContainerPump(t *testing.T) { 206 | config := &docker.Config{ 207 | Tty: false, 208 | } 209 | container := &docker.Container{ 210 | ID: "8dfafdbc3a40", 211 | Config: config, 212 | } 213 | pump := newContainerPump(container, os.Stdout, os.Stderr) 214 | if pump == nil { 215 | t.Error("pump nil") 216 | return 217 | } 218 | } 219 | 220 | func TestPumpContainerPump(t *testing.T) { 221 | config := &docker.Config{ 222 | Tty: true, 223 | } 224 | container := &docker.Container{ 225 | ID: "8dfafdbc3a40", 226 | Config: config, 227 | } 228 | pump := newContainerPump(container, os.Stdout, os.Stderr) 229 | logstream, route := make(chan *Message), &Route{} 230 | go func() { 231 | for msg := range logstream { 232 | t.Logf("message: %+v", msg) 233 | } 234 | }() 235 | pump.add(logstream, route) 236 | if pump.logstreams[logstream] != route { 237 | t.Error("expected pump to contain logstream matching route") 238 | } 239 | pump.send(&Message{Data: "test data"}) 240 | 241 | pump.remove(logstream) 242 | if pump.logstreams[logstream] != nil { 243 | t.Error("logstream should have been removed") 244 | } 245 | } 246 | 247 | func TestPumpRoutingFrom(t *testing.T) { 248 | container := &docker.Container{ 249 | ID: "8dfafdbc3a40", 250 | } 251 | p := &LogsPump{ 252 | pumps: make(map[string]*containerPump), 253 | routes: make(map[chan *update]struct{}), 254 | } 255 | 256 | if p.RoutingFrom(container.ID) != false { 257 | t.Errorf("expected RoutingFrom to return 'false'") 258 | } 259 | 260 | p.pumps[container.ID] = nil 261 | if p.RoutingFrom(container.ID) != true { 262 | t.Errorf("expected RoutingFrom to return 'true'") 263 | } 264 | if p.RoutingFrom("") != false { 265 | t.Errorf("expected RoutingFrom to return 'false'") 266 | } 267 | if p.RoutingFrom("foo") != false { 268 | t.Errorf("expected RoutingFrom to return 'false'") 269 | } 270 | } 271 | 272 | func TestPumpBacklog(t *testing.T) { 273 | os.Unsetenv("BACKLOG") 274 | if backlog() != false { 275 | t.Errorf("expected backlog() to return 'false'") 276 | } 277 | os.Setenv("BACKLOG", "false") 278 | if backlog() != false { 279 | t.Errorf("expected backlog() to return 'false'") 280 | } 281 | os.Setenv("BACKLOG", "true") 282 | if backlog() != true { 283 | t.Errorf("expected backlog() to return 'true'") 284 | } 285 | os.Unsetenv("BACKLOG") 286 | if backlog() != false { 287 | t.Errorf("expected backlog() to return 'false'") 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /router/routes.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "crypto/sha1" //nolint:gosec 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/url" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/gliderlabs/logspout/cfg" 17 | ) 18 | 19 | // Routes is all the configured routes 20 | var Routes *RouteManager 21 | 22 | func init() { 23 | Routes = &RouteManager{routes: make(map[string]*Route)} 24 | Jobs.Register(Routes, "routes") 25 | } 26 | 27 | // RouteManager is responsible for maintaining route state 28 | type RouteManager struct { 29 | sync.Mutex 30 | persistor RouteStore 31 | routes map[string]*Route 32 | routing bool 33 | wg sync.WaitGroup 34 | } 35 | 36 | // Load loads all route from a RouteStore 37 | func (rm *RouteManager) Load(persistor RouteStore) error { 38 | routes, err := persistor.GetAll() 39 | if err != nil { 40 | return err 41 | } 42 | for _, route := range routes { 43 | if err = rm.Add(route); err != nil { 44 | return err 45 | } 46 | } 47 | rm.persistor = persistor 48 | return nil 49 | } 50 | 51 | // Get returns a Route based on id 52 | func (rm *RouteManager) Get(id string) (*Route, error) { 53 | rm.Lock() 54 | defer rm.Unlock() 55 | route, ok := rm.routes[id] 56 | if !ok { 57 | return nil, os.ErrNotExist 58 | } 59 | return route, nil 60 | } 61 | 62 | // GetAll returns all routes in the RouteManager 63 | func (rm *RouteManager) GetAll() ([]*Route, error) { 64 | rm.Lock() 65 | defer rm.Unlock() 66 | routes := make([]*Route, 0) 67 | for _, route := range rm.routes { 68 | routes = append(routes, route) 69 | } 70 | return routes, nil 71 | } 72 | 73 | // Remove removes a route from a RouteManager based on id 74 | func (rm *RouteManager) Remove(id string) bool { 75 | rm.Lock() 76 | defer rm.Unlock() 77 | route, ok := rm.routes[id] 78 | if ok && route.closer != nil { 79 | route.closer <- struct{}{} 80 | } 81 | delete(rm.routes, id) 82 | if rm.persistor != nil { 83 | rm.persistor.Remove(id) 84 | } 85 | return ok 86 | } 87 | 88 | // AddFromURI creates a new route from an URI string and adds it to the RouteManager 89 | func (rm *RouteManager) AddFromURI(uri string) error { 90 | expandedRoute := os.ExpandEnv(uri) 91 | u, err := url.Parse(expandedRoute) 92 | if err != nil { 93 | return err 94 | } 95 | r := &Route{ 96 | Address: u.Host, 97 | Adapter: u.Scheme, 98 | Options: make(map[string]string), 99 | } 100 | if u.RawQuery != "" { 101 | params, err := url.ParseQuery(u.RawQuery) 102 | if err != nil { 103 | return err 104 | } 105 | for key := range params { 106 | value := params.Get(key) 107 | switch key { 108 | case "filter.id": 109 | r.FilterID = value 110 | case "filter.name": 111 | r.FilterName = value 112 | case "filter.labels": 113 | r.FilterLabels = strings.Split(value, ",") 114 | case "filter.sources": 115 | r.FilterSources = strings.Split(value, ",") 116 | default: 117 | r.Options[key] = value 118 | } 119 | } 120 | } 121 | return rm.Add(r) 122 | } 123 | 124 | // Add adds a route to the RouteManager 125 | func (rm *RouteManager) Add(route *Route) error { 126 | rm.Lock() 127 | defer rm.Unlock() 128 | factory, found := AdapterFactories.Lookup(route.AdapterType()) 129 | if !found { 130 | return errors.New("bad adapter: " + route.Adapter) 131 | } 132 | adapter, err := factory(route) 133 | if err != nil { 134 | return err 135 | } 136 | if route.ID == "" { 137 | h := sha1.New() //nolint:gosec 138 | io.WriteString(h, strconv.Itoa(int(time.Now().UnixNano()))) 139 | route.ID = fmt.Sprintf("%x", h.Sum(nil))[:12] 140 | } 141 | route.closer = make(chan struct{}) 142 | route.adapter = adapter 143 | // Stop any existing route with this ID: 144 | if rm.routes[route.ID] != nil { 145 | rm.routes[route.ID].closer <- struct{}{} 146 | } 147 | 148 | rm.routes[route.ID] = route 149 | if rm.persistor != nil { 150 | if err := rm.persistor.Add(route); err != nil { 151 | log.Println("persistor:", err) 152 | } 153 | } 154 | if rm.routing { 155 | go rm.route(route) 156 | } 157 | return nil 158 | } 159 | 160 | func (rm *RouteManager) route(route *Route) { 161 | logstream := make(chan *Message) 162 | defer route.Close() 163 | rm.Route(route, logstream) 164 | route.adapter.Stream(logstream) 165 | } 166 | 167 | // Route takes a logstream and route and passes them off to all configure LogRouters 168 | func (rm *RouteManager) Route(route *Route, logstream chan *Message) { 169 | for _, router := range LogRouters.All() { 170 | go router.Route(route, logstream) 171 | } 172 | } 173 | 174 | // RoutingFrom returns whether a given container is routing through the RouteManager 175 | func (rm *RouteManager) RoutingFrom(containerID string) bool { 176 | for _, router := range LogRouters.All() { 177 | if router.RoutingFrom(containerID) { 178 | return true 179 | } 180 | } 181 | return false 182 | } 183 | 184 | // Run executes the RouteManager 185 | func (rm *RouteManager) Run() error { 186 | rm.Lock() 187 | for _, route := range rm.routes { 188 | rm.wg.Add(1) 189 | go func(route *Route) { 190 | rm.route(route) 191 | rm.wg.Done() 192 | }(route) 193 | } 194 | rm.routing = true 195 | rm.Unlock() 196 | rm.wg.Wait() 197 | // Temp fix to allow logspout to run without routes defined. 198 | if len(rm.routes) == 0 { 199 | select {} 200 | } 201 | return nil 202 | } 203 | 204 | // Name returns the name of the RouteManager 205 | func (rm *RouteManager) Name() string { 206 | return "routes" 207 | } 208 | 209 | // Setup configures the RouteManager 210 | func (rm *RouteManager) Setup() error { 211 | var uris string 212 | if os.Getenv("ROUTE_URIS") != "" { 213 | uris = os.Getenv("ROUTE_URIS") 214 | } 215 | if len(os.Args) > 1 { 216 | uris = os.Args[1] 217 | } 218 | if uris != "" { 219 | for _, uri := range strings.Split(uris, ",") { 220 | err := rm.AddFromURI(uri) 221 | if err != nil { 222 | return err 223 | } 224 | } 225 | } 226 | 227 | persistPath := cfg.GetEnvDefault("ROUTESPATH", "/mnt/routes") 228 | if _, err := os.Stat(persistPath); err == nil { 229 | return rm.Load(RouteFileStore(persistPath)) 230 | } 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /router/routes_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type DummyAdapter struct{} 9 | 10 | func (a *DummyAdapter) Stream(logstream chan *Message) { 11 | for message := range logstream { 12 | print("message passed to DummyAdapter: ", message) 13 | } 14 | } 15 | 16 | func newDummyAdapter(route *Route) (LogAdapter, error) { 17 | return &DummyAdapter{}, nil 18 | } 19 | 20 | func TestRouterGetAll(t *testing.T) { 21 | rts, err := Routes.GetAll() 22 | if err != nil { 23 | t.Error("error getting all routes") 24 | } 25 | routes := append(marshal(rts), '\n') 26 | emptyRoutes := append(marshal(make([]*Route, 0)), '\n') 27 | if !reflect.DeepEqual(routes, emptyRoutes) { 28 | t.Error("expected '[]' got:", routes) 29 | } 30 | } 31 | 32 | func TestRouterNoDuplicateIds(t *testing.T) { 33 | AdapterFactories.Register(newDummyAdapter, "syslog") 34 | 35 | // Mock "running" so routes actually start running when added. 36 | Routes.routing = true 37 | 38 | // Start the first route. 39 | route1 := &Route{ 40 | ID: "abc", 41 | Address: "someUrl", 42 | Adapter: "syslog", 43 | } 44 | if err := Routes.Add(route1); err != nil { 45 | t.Error("Error adding route:", err) 46 | } 47 | 48 | // Start a second route with the same ID. 49 | var route2 = &Route{ 50 | ID: "abc", 51 | Address: "someUrl2", 52 | Adapter: "syslog", 53 | } 54 | Routes.Add(route2) 55 | 56 | if !route1.closed { 57 | t.Errorf("route1 was not closed after route2 added.") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /router/types.go: -------------------------------------------------------------------------------- 1 | //go:generate go-extpoints . AdapterFactory HttpHandler AdapterTransport LogRouter Job 2 | package router 3 | 4 | import ( 5 | "net" 6 | "net/http" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | docker "github.com/fsouza/go-dockerclient" 12 | ) 13 | 14 | // HTTPHandler is an extension type for adding HTTP endpoints 15 | type HTTPHandler func() http.Handler 16 | 17 | // AdapterFactory is an extension type for adding new log adapters 18 | type AdapterFactory func(route *Route) (LogAdapter, error) 19 | 20 | // AdapterTransport is an extension type for connection transports used by adapters 21 | type AdapterTransport interface { 22 | Dial(addr string, options map[string]string) (net.Conn, error) 23 | } 24 | 25 | // LogAdapter is a streamed log 26 | type LogAdapter interface { 27 | Stream(logstream chan *Message) 28 | } 29 | 30 | // Job is a thing to be done 31 | type Job interface { 32 | Run() error 33 | Setup() error 34 | Name() string 35 | } 36 | 37 | // LogRouter sends logs to LogAdapters via Routes 38 | type LogRouter interface { 39 | RoutingFrom(containerID string) bool 40 | Route(route *Route, logstream chan *Message) 41 | } 42 | 43 | // RouteStore is a collections of Routes 44 | type RouteStore interface { 45 | Get(id string) (*Route, error) 46 | GetAll() ([]*Route, error) 47 | Add(route *Route) error 48 | Remove(id string) bool 49 | } 50 | 51 | // Message is a log messages 52 | type Message struct { 53 | Container *docker.Container 54 | Source string 55 | Data string 56 | Time time.Time 57 | } 58 | 59 | // Route represents what subset of logs should go where 60 | type Route struct { 61 | ID string `json:"id"` 62 | FilterID string `json:"filter_id,omitempty"` 63 | FilterName string `json:"filter_name,omitempty"` 64 | FilterSources []string `json:"filter_sources,omitempty"` 65 | FilterLabels []string `json:"filter_labels,omitempty"` 66 | Adapter string `json:"adapter"` 67 | Address string `json:"address"` 68 | Options map[string]string `json:"options,omitempty"` 69 | adapter LogAdapter 70 | closed bool 71 | closer chan struct{} 72 | closerRcv <-chan struct{} // used instead of closer when set 73 | } 74 | 75 | // AdapterType returns a route's adapter type string 76 | func (r *Route) AdapterType() string { 77 | return strings.Split(r.Adapter, "+")[0] 78 | } 79 | 80 | // AdapterTransport returns a route's adapter transport string 81 | func (r *Route) AdapterTransport(dfault string) string { 82 | parts := strings.Split(r.Adapter, "+") 83 | if len(parts) > 1 { 84 | return parts[1] 85 | } 86 | return dfault 87 | } 88 | 89 | // Closer returns a route's closerRcv 90 | func (r *Route) Closer() <-chan struct{} { 91 | if r.closerRcv != nil { 92 | return r.closerRcv 93 | } 94 | return r.closer 95 | } 96 | 97 | // OverrideCloser sets a Route.closer to closer 98 | func (r *Route) OverrideCloser(closer <-chan struct{}) { 99 | r.closerRcv = closer 100 | } 101 | 102 | // Close sends true to a Route.closer 103 | func (r *Route) Close() { 104 | r.closer <- struct{}{} 105 | } 106 | 107 | func (r *Route) matchAll() bool { 108 | if r.FilterID == "" && r.FilterName == "" && len(r.FilterSources) == 0 && len(r.FilterLabels) == 0 { 109 | return true 110 | } 111 | return false 112 | } 113 | 114 | // MultiContainer returns whether the Route is matching multiple containers or not 115 | func (r *Route) MultiContainer() bool { 116 | return r.matchAll() || strings.Contains(r.FilterName, "*") 117 | } 118 | 119 | // MatchContainer returns whether the Route is responsible for a given container 120 | func (r *Route) MatchContainer(id, name string, labels map[string]string) bool { 121 | if r.matchAll() { 122 | return true 123 | } 124 | if r.FilterID != "" && !strings.HasPrefix(id, r.FilterID) { 125 | return false 126 | } 127 | match, err := path.Match(r.FilterName, name) 128 | if err != nil || (r.FilterName != "" && !match) { 129 | return false 130 | } 131 | for _, label := range r.FilterLabels { 132 | labelParts := strings.SplitN(label, ":", 2) 133 | if len(labelParts) > 1 { 134 | labelKey := labelParts[0] 135 | labelValue := labelParts[1] 136 | labelMatch, labelErr := path.Match(labelValue, labels[labelKey]) 137 | if labelErr != nil || (labelValue != "" && !labelMatch) { 138 | return false 139 | } 140 | } 141 | } 142 | 143 | return true 144 | } 145 | 146 | // MatchMessage returns whether the Route is responsible for a given Message 147 | func (r *Route) MatchMessage(message *Message) bool { 148 | if r.matchAll() { 149 | return true 150 | } 151 | if len(r.FilterSources) > 0 && !contains(r.FilterSources, message.Source) { 152 | return false 153 | } 154 | return true 155 | } 156 | 157 | func contains(strs []string, str string) bool { 158 | for _, s := range strs { 159 | if s == str { 160 | return true 161 | } 162 | } 163 | return false 164 | } 165 | -------------------------------------------------------------------------------- /routesapi/README.md: -------------------------------------------------------------------------------- 1 | # routesapi 2 | 3 | ### Routes Resource 4 | 5 | Routes let you configure logspout to hand-off logs to another system using logspout adapters, such as syslog. 6 | 7 | #### Creating a route 8 | 9 | POST /routes 10 | 11 | Takes a JSON object like this: 12 | 13 | { 14 | "adapter": "syslog", 15 | "address": "logaggregator.service.consul", 16 | "filter_name": "*_db", 17 | "filter_sources": ["stdout"], 18 | "filter_labels": ["com.example.foo:bar*"], 19 | "options": { 20 | "append_tag": ".db" 21 | } 22 | } 23 | 24 | The main fields are `adapter` and `address`. The field `options` is passed to the adapter. There are four filter fields: `filter_name`, `filter_sources`, `filter_id`, and `filter_labels`. These let you limit which containers or types of logs to route. Use `filter_id` to limit to a particular container by ID. Use `filter_name` to match against container names. These can include wildcards. Use `filter_sources` to limit to `stdout` or `stderr`, or soon `syslog`. Use `filter_labels` to limit containers to require specific labels. These can include wildcards. 25 | 26 | To route all logs of all types on all containers, don't specify any filter values. 27 | 28 | The `append_tag` field of `options` is adapter specific to `syslog`. It lets you append to the tag of syslog packets for this route. By default the tag is ``, so an `append_tag` value of `.app` would make the tag `.app`. 29 | 30 | And yes, you can just specify an IP and port for `address`, but you can also specify a name that resolves via DNS to one or more SRV records. That means this works great with [Consul](http://www.consul.io/) for service discovery. 31 | 32 | #### Listing routes 33 | 34 | GET /routes 35 | 36 | Returns a JSON list of current routes: 37 | 38 | [ 39 | { 40 | "id": "3631c027fb1b", 41 | "filter_name": "mycontainer", 42 | "adapter": "syslog", 43 | "address": "192.168.1.111:514" 44 | } 45 | ] 46 | 47 | #### Viewing a route 48 | 49 | GET /routes/ 50 | 51 | Returns a JSON route object: 52 | 53 | { 54 | "id": "3631c027fb1b", 55 | "filter_id": "a9efd0aeb470", 56 | "filter_sources": ["stderr"], 57 | "adapter": "syslog", 58 | "address": "192.168.1.111:514" 59 | } 60 | 61 | #### Deleting a route 62 | 63 | DELETE /routes/ 64 | -------------------------------------------------------------------------------- /routesapi/routesapi.go: -------------------------------------------------------------------------------- 1 | package routesapi 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/gorilla/mux" 10 | 11 | "github.com/gliderlabs/logspout/router" 12 | ) 13 | 14 | func init() { 15 | router.HTTPHandlers.Register(RoutesAPI, "routes") 16 | } 17 | 18 | // RoutesAPI returns a handler for the routes API 19 | func RoutesAPI() http.Handler { 20 | routes := router.Routes 21 | r := mux.NewRouter() 22 | 23 | r.HandleFunc("/routes/{id}", func(w http.ResponseWriter, req *http.Request) { 24 | params := mux.Vars(req) 25 | route, _ := routes.Get(params["id"]) 26 | if route == nil { 27 | http.NotFound(w, req) 28 | return 29 | } 30 | w.Write(append(marshal(route), '\n')) 31 | }).Methods("GET") 32 | 33 | r.HandleFunc("/routes/{id}", func(w http.ResponseWriter, req *http.Request) { 34 | params := mux.Vars(req) 35 | if ok := routes.Remove(params["id"]); !ok { 36 | http.NotFound(w, req) 37 | } 38 | }).Methods("DELETE") 39 | 40 | r.HandleFunc("/routes", func(w http.ResponseWriter, req *http.Request) { 41 | w.Header().Add("Content-Type", "application/json") 42 | rts, _ := routes.GetAll() 43 | w.Write(append(marshal(rts), '\n')) 44 | }).Methods("GET") 45 | 46 | r.HandleFunc("/routes", func(w http.ResponseWriter, req *http.Request) { 47 | route := new(router.Route) 48 | if err := unmarshal(req.Body, route); err != nil { 49 | http.Error(w, "Bad request: "+err.Error(), http.StatusBadRequest) 50 | return 51 | } 52 | err := routes.Add(route) 53 | if err != nil { 54 | http.Error(w, "Bad route: "+err.Error(), http.StatusBadRequest) 55 | return 56 | } 57 | w.Header().Add("Content-Type", "application/json") 58 | w.WriteHeader(http.StatusCreated) 59 | w.Write(append(marshal(route), '\n')) 60 | }).Methods("POST") 61 | 62 | return r 63 | } 64 | 65 | func marshal(obj interface{}) []byte { 66 | bytes, err := json.MarshalIndent(obj, "", " ") 67 | if err != nil { 68 | log.Println("marshal:", err) 69 | } 70 | return bytes 71 | } 72 | 73 | func unmarshal(input io.Reader, obj interface{}) error { 74 | dec := json.NewDecoder(input) 75 | return dec.Decode(obj) 76 | } 77 | -------------------------------------------------------------------------------- /run-custom.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | ./build-custom.sh 4 | docker build --file Dockerfile.custom -t mylogspouter . 5 | docker run --rm --name=logspout \ 6 | -v=/var/run/docker.sock:/var/run/docker.sock \ 7 | -p 8000:80 \ 8 | mylogspouter \ 9 | ${SYSLOG} 10 | -------------------------------------------------------------------------------- /transports/tcp/tcp.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/gliderlabs/logspout/adapters/raw" 7 | "github.com/gliderlabs/logspout/router" 8 | ) 9 | 10 | func init() { 11 | router.AdapterTransports.Register(new(tcpTransport), "tcp") 12 | // convenience adapters around raw adapter 13 | router.AdapterFactories.Register(rawTCPAdapter, "tcp") 14 | } 15 | 16 | func rawTCPAdapter(route *router.Route) (router.LogAdapter, error) { 17 | route.Adapter = "raw+tcp" 18 | return raw.NewRawAdapter(route) 19 | } 20 | 21 | type tcpTransport int 22 | 23 | func (t *tcpTransport) Dial(addr string, options map[string]string) (net.Conn, error) { 24 | raddr, err := net.ResolveTCPAddr("tcp", addr) 25 | if err != nil { 26 | return nil, err 27 | } 28 | conn, err := net.DialTCP("tcp", nil, raddr) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return conn, nil 33 | } 34 | -------------------------------------------------------------------------------- /transports/tls/testdata/ca_int.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC6zCCAkygAwIBAgIUbDsX3DH6BgavfEiHUEjIopR/fS4wCgYIKoZIzj0EAwQw 3 | gY0xCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMRAwDgYDVQQHEwdUb3Jv 4 | bnRvMREwDwYDVQQKEwhsaW51eGN0bDEMMAoGA1UECxMDTGFiMTkwNwYDVQQDEzBs 5 | aW51eGN0bCBFQ0MgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAoVGVzdCkw 6 | HhcNMTgwODA2MTYwNjAwWhcNMjgwODAzMTYwNjAwWjCBlTELMAkGA1UEBhMCQ0Ex 7 | EDAOBgNVBAgTB09udGFyaW8xEDAOBgNVBAcTB1Rvcm9udG8xETAPBgNVBAoTCGxp 8 | bnV4Y3RsMQwwCgYDVQQLEwNMYWIxQTA/BgNVBAMTOGxpbnV4Y3RsIEVDQyBJbnRl 9 | cm1lZGlhdGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKFRlc3QpMHYwEAYHKoZI 10 | zj0CAQYFK4EEACIDYgAEumE3EaWlG+3gV9hZKMknoS5BAiR26qjFBRq/GVE3Ox36 11 | eqOo7+PerP1joMktjf4NNciUR6F1WrlJWWoz/HnEbo5GwxH2JrS6OIazYtHZtgTJ 12 | CJ2yRttpYzZmJA/pKkiQo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw 13 | AwEB/zAdBgNVHQ4EFgQUqBhG75wUMIWEQwVlA8btbgBBLgcwHwYDVR0jBBgwFoAU 14 | 5xOpIUH9aTCZKIXVXTB6GtMvDgkwCgYIKoZIzj0EAwQDgYwAMIGIAkIBUjnM8GwU 15 | AHOXNyOaRDXl4g21j6qCNe+GNlt69eBG8iOI+uKCyxS1K1urZqCSW1YtpCEZT0x4 16 | IehI1//gww5+iwcCQgF+MVUo8TEyrSaySP0Ubng59XbBcVdveYgPvGZ1isgPuPIa 17 | DgJFxBzMfB0kKhOyEjTcW2KmIpCi0UIXfJMncJIJIw== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /transports/tls/testdata/ca_root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC5zCCAkmgAwIBAgIUSGqeayU7K7Il+Ctlrrt5HCnuPzswCgYIKoZIzj0EAwQw 3 | gY0xCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMRAwDgYDVQQHEwdUb3Jv 4 | bnRvMREwDwYDVQQKEwhsaW51eGN0bDEMMAoGA1UECxMDTGFiMTkwNwYDVQQDEzBs 5 | aW51eGN0bCBFQ0MgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAoVGVzdCkw 6 | HhcNMTgwODA2MTYwNjAwWhcNNDgwNzI5MTYwNjAwWjCBjTELMAkGA1UEBhMCQ0Ex 7 | EDAOBgNVBAgTB09udGFyaW8xEDAOBgNVBAcTB1Rvcm9udG8xETAPBgNVBAoTCGxp 8 | bnV4Y3RsMQwwCgYDVQQLEwNMYWIxOTA3BgNVBAMTMGxpbnV4Y3RsIEVDQyBSb290 9 | IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IChUZXN0KTCBmzAQBgcqhkjOPQIBBgUr 10 | gQQAIwOBhgAEAX6CA41WCLFZfAJGWOe5IKYRl0JNPzVKTrS9A2n0aBpR3X9mf9WB 11 | FMQS6zBHDWb0H+7IMLLkE5TP7grW1Kc3732HAZcbQgABhvSKEWDzZqojWffXyr3g 12 | Y+IS7nkpez8c2pxC29xKMvvxPr1w3kU3+KhfOZILZss0pudNVqNbLzG/SC5po0Iw 13 | QDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5xOp 14 | IUH9aTCZKIXVXTB6GtMvDgkwCgYIKoZIzj0EAwQDgYsAMIGHAkIBT8Pd+5hIQVCy 15 | oIOGRgcRVk6YfkFSK/t3j9zjhDsoAqqsQfGa4wY62c0jT3nzPognx7I0aAyN9SH3 16 | yrj5Ay14PPkCQRItmEcEDvLmteNN8iuLqymtyY/8+3eRC2Wwfr430pfa6lTB4oxf 17 | 84w73oP8uutvV7JboQehpD9ph1p/yLvwb18V 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /transports/tls/testdata/client_logspoutClient-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDAJcsSf4T2jyQF2Kf1XSLYDLtCsEC8z9tmUbDjsYuvZzEAgSgoria3X 3 | yPSUwjkOGi+gBwYFK4EEACKhZANiAASr86T5K/WjLVTWHOLwjopoqutS73/R4IPY 4 | Q60re8X1CTNyuedzFz96TeQKIP2rnqsMgBYv+AmiuNQ9mpUL0W9P3xdGDOl+XFy0 5 | Pu23bCDfkhYXc0kpwg4GthP+V2OIldA= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /transports/tls/testdata/client_logspoutClient.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC0jCCAligAwIBAgIUOkWdcHs6jH+FXx4OTkWVVIvkbeAwCgYIKoZIzj0EAwMw 3 | gZUxCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMRAwDgYDVQQHEwdUb3Jv 4 | bnRvMREwDwYDVQQKEwhsaW51eGN0bDEMMAoGA1UECxMDTGFiMUEwPwYDVQQDEzhs 5 | aW51eGN0bCBFQ0MgSW50ZXJtZWRpYXRlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 6 | IChUZXN0KTAeFw0xODA4MDYxNjA2MDBaFw0xOTA4MDYxNjA2MDBaMGsxCzAJBgNV 7 | BAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMRAwDgYDVQQHEwdUb3JvbnRvMREwDwYD 8 | VQQKEwhsaW51eGN0bDEMMAoGA1UECxMDTGFiMRcwFQYDVQQDEw5sb2dzcG91dENs 9 | aWVudDB2MBAGByqGSM49AgEGBSuBBAAiA2IABKvzpPkr9aMtVNYc4vCOimiq61Lv 10 | f9Hgg9hDrSt7xfUJM3K553MXP3pN5Aog/aueqwyAFi/4CaK41D2alQvRb0/fF0YM 11 | 6X5cXLQ+7bdsIN+SFhdzSSnCDga2E/5XY4iV0KOBkTCBjjAOBgNVHQ8BAf8EBAMC 12 | BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU 13 | MTCDzk67r1vQWeLrGSptxmlGDIMwHwYDVR0jBBgwFoAUqBhG75wUMIWEQwVlA8bt 14 | bgBBLgcwGQYDVR0RBBIwEIIObG9nc3BvdXRDbGllbnQwCgYIKoZIzj0EAwMDaAAw 15 | ZQIxAM2WRDo5n6l19YyfY4lGXALGZrjoAw7Mhob7YTjxm31CFOKiUNMOXR9wsyrC 16 | 2Mot9gIwbg23r05wSLNEa3f/UY7IxuXjKoJDbde+BU16NCFxaqzUQGI+xEYWIZ1M 17 | V1s2+DQ+ 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /transports/tls/testdata/server_loggingEndpoint-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDBtJFMhliy0eCKw34DfTpd+2e3n8f5qEVr575r+KpMb5Gq35sE4HQbq 3 | IGv4ResndQGgBwYFK4EEACKhZANiAASTzfLNpufxzvCpbJozEvDc8Xt7uqLGdODU 4 | 0MEQyFPj6d3qj25KWDJzPhZvmZvXVGbizTCrOXSbgyaHX0MwkQJT3uhFDzlbzrez 5 | I+iPHkgDteeeSwR8jwJorFpWUiSpOc4= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /transports/tls/testdata/server_loggingEndpoint.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC7DCCAnKgAwIBAgIUZCOnh3N7v2QeIQr+BnaEgfsK/00wCgYIKoZIzj0EAwMw 3 | gZUxCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMRAwDgYDVQQHEwdUb3Jv 4 | bnRvMREwDwYDVQQKEwhsaW51eGN0bDEMMAoGA1UECxMDTGFiMUEwPwYDVQQDEzhs 5 | aW51eGN0bCBFQ0MgSW50ZXJtZWRpYXRlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 6 | IChUZXN0KTAeFw0xODA4MDYxNjA2MDBaFw0yODA4MDMxNjA2MDBaMGwxCzAJBgNV 7 | BAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMRAwDgYDVQQHEwdUb3JvbnRvMREwDwYD 8 | VQQKEwhsaW51eGN0bDEMMAoGA1UECxMDTGFiMRgwFgYDVQQDEw9sb2dnaW5nRW5k 9 | cG9pbnQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASTzfLNpufxzvCpbJozEvDc8Xt7 10 | uqLGdODU0MEQyFPj6d3qj25KWDJzPhZvmZvXVGbizTCrOXSbgyaHX0MwkQJT3uhF 11 | DzlbzrezI+iPHkgDteeeSwR8jwJorFpWUiSpOc6jgaowgacwDgYDVR0PAQH/BAQD 12 | AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE 13 | FKdWv3s0qjyaEO9EqlCeoAcknC3gMB8GA1UdIwQYMBaAFKgYRu+cFDCFhEMFZQPG 14 | 7W4AQS4HMDIGA1UdEQQrMCmCCWxvY2FsaG9zdIIWbG9ncy50ZXN0LmxpbnV4Y3Rs 15 | LmNvbYcEfwAAATAKBggqhkjOPQQDAwNoADBlAjEA5slZL+k4+po5SvESvfhY1ybN 16 | dJTAaoSje4Nrb9z9Moc8qkRGmcXOA1sfcdDjMXM6AjBRK6rx9JxbZeOjglS6pFVY 17 | 2VPbxm/GrdVMt1cGm3G3oPn7IgjBRZ7KhGPweT81G4w= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /transports/tls/tls.go: -------------------------------------------------------------------------------- 1 | // +build go1.8 2 | 3 | package tls 4 | 5 | import ( 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "os" 13 | "strings" 14 | 15 | "github.com/gliderlabs/logspout/adapters/raw" 16 | "github.com/gliderlabs/logspout/router" 17 | ) 18 | 19 | const ( 20 | // constants used to identify environment variable names 21 | envDisableSystemRoots = "LOGSPOUT_TLS_DISABLE_SYSTEM_ROOTS" 22 | envCaCerts = "LOGSPOUT_TLS_CA_CERTS" 23 | envClientCert = "LOGSPOUT_TLS_CLIENT_CERT" 24 | envClientKey = "LOGSPOUT_TLS_CLIENT_KEY" 25 | envTLSHardening = "LOGSPOUT_TLS_HARDENING" 26 | trueString = "true" 27 | ) 28 | 29 | var ( 30 | // package wide cache of TLS config 31 | clientTLSConfig *tls.Config 32 | // PCI compliance as of Jun 30, 2018: anything under TLS 1.1 must be disabled 33 | // we bump this up to TLS 1.2 so we can support best possible ciphers 34 | hardenedMinVersion = uint16(tls.VersionTLS12) 35 | // allowed ciphers when in hardened mode 36 | // disable CBC suites (Lucky13 attack) this means TLS 1.1 can't work (no GCM) 37 | // only use perfect forward secrecy ciphers 38 | hardenedCiphers = []uint16{ 39 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 40 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 41 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 42 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 43 | // these ciphers require go 1.8+ 44 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 45 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 46 | } 47 | // EC curve preference when in hardened mode 48 | // curve reference: http://safecurves.cr.yp.to/ 49 | hardenedCurvePreferences = []tls.CurveID{ 50 | // this curve is a non-NIST curve with no NSA influence. Prefer this over all others! 51 | // this curve required go 1.8+ 52 | tls.X25519, 53 | // These curves are provided by NIST; prefer in descending order 54 | tls.CurveP521, 55 | tls.CurveP384, 56 | tls.CurveP256, 57 | } 58 | ) 59 | 60 | type tlsTransport int 61 | 62 | func init() { 63 | router.AdapterTransports.Register(new(tlsTransport), "tls") 64 | // convenience adapters around raw adapter 65 | router.AdapterFactories.Register(rawTLSAdapter, "tls") 66 | 67 | // we should load our TLS configuration only once 68 | // since it is not expected to change during runtime 69 | var err error 70 | if clientTLSConfig, err = createTLSConfig(); err != nil { 71 | // without a valid/desired TLS config, we should exit 72 | log.Fatalf("error with TLSConfig: %s", err) 73 | } 74 | } 75 | 76 | func rawTLSAdapter(route *router.Route) (r router.LogAdapter, err error) { 77 | route.Adapter = "raw+tls" 78 | r, err = raw.NewRawAdapter(route) 79 | return 80 | } 81 | 82 | func (t *tlsTransport) Dial(addr string, options map[string]string) (conn net.Conn, err error) { 83 | // at this point, if our trust store is empty, there is no point of continuing 84 | // since it would be impossible to successfully validate any x509 server certificates 85 | if len(clientTLSConfig.RootCAs.Subjects()) < 1 { 86 | err = fmt.Errorf("FATAL: TLS CA trust store is empty! Can not trust any TLS endpoints: tls://%s", addr) 87 | return 88 | } 89 | 90 | // attempt to establish the TLS connection 91 | conn, err = tls.Dial("tcp", addr, clientTLSConfig) 92 | return 93 | } 94 | 95 | // createTLSConfig creates the required TLS configuration that we need to establish a TLS connection 96 | func createTLSConfig() (tlsConfig *tls.Config, err error) { 97 | tlsConfig = &tls.Config{} 98 | 99 | // use stronger TLS settings if enabled 100 | // perhaps this should be default setting @gbolo 101 | if os.Getenv(envTLSHardening) == trueString { 102 | tlsConfig.InsecureSkipVerify = false 103 | tlsConfig.MinVersion = hardenedMinVersion 104 | tlsConfig.CipherSuites = hardenedCiphers 105 | tlsConfig.CurvePreferences = hardenedCurvePreferences 106 | } 107 | 108 | // load possible TLS CA chain(s) for server certificate validation 109 | // starting with an empty pool 110 | tlsConfig.RootCAs = x509.NewCertPool() 111 | 112 | // load system root CA trust store by default, unless configured not to 113 | // if we cannot, then it's fatal. 114 | // NOTE that we ONLY fail if SystemCertPool returns an error, 115 | // not if our system trust store is empty or doesn't exist! 116 | if os.Getenv(envDisableSystemRoots) != trueString { 117 | tlsConfig.RootCAs, err = x509.SystemCertPool() 118 | if err != nil { 119 | return 120 | } 121 | } 122 | 123 | // load custom certificates specified by configuration: 124 | // we expect a comma separated list of certificate file paths 125 | // if we fail to load a certificate, we should treat this to be fatal 126 | // as the user may not wish to send logs through an untrusted TLS connection 127 | // also note that each file specified above can contain one or more certificates 128 | // and we also _DO NOT_ check if they are CA certificates (in case of self-signed) 129 | if certsEnv := os.Getenv(envCaCerts); certsEnv != "" { 130 | certFilePaths := strings.Split(certsEnv, ",") 131 | for _, certFilePath := range certFilePaths { 132 | // each pem file may contain more than one certficate 133 | var certBytes []byte 134 | certBytes, err = ioutil.ReadFile(certFilePath) 135 | if err != nil { 136 | return 137 | } 138 | if !tlsConfig.RootCAs.AppendCertsFromPEM(certBytes) { 139 | err = fmt.Errorf("failed to load CA certificate(s): %s", certFilePath) 140 | return 141 | } 142 | } 143 | } 144 | 145 | // load a client certificate and key if enabled 146 | // we should only attempt this if BOTH cert and key are defined 147 | clientCertFilePath := os.Getenv(envClientCert) 148 | clientKeyFilePath := os.Getenv(envClientKey) 149 | if clientCertFilePath != "" && clientKeyFilePath != "" { 150 | var clientCert tls.Certificate 151 | clientCert, err = tls.LoadX509KeyPair(clientCertFilePath, clientKeyFilePath) 152 | // we should fail if unable to load the keypair since the user intended mutual authentication 153 | if err != nil { 154 | return 155 | } 156 | // according to TLS spec (RFC 5246 appendix F.1.1) the certificate message 157 | // must provide a valid certificate chain leading to an acceptable certificate authority. 158 | // We will make this optional; the client cert pem file can contain more than one certificate 159 | tlsConfig.Certificates = []tls.Certificate{clientCert} 160 | } 161 | return //nolint:nakedret 162 | } 163 | -------------------------------------------------------------------------------- /transports/tls/tls_test.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | // these constants may need to change when testdata content changes 12 | caRootCertFileLocation = "./testdata/ca_root.pem" 13 | caRootCertSubjectCN = "linuxctl ECC Root Certification Authority (Test)" 14 | caIntCertFileLocation = "./testdata/ca_int.pem" 15 | caIntCertSubjectCN = "linuxctl ECC Intermediate Certification Authority (Test)" 16 | clientCertFileLocation = "./testdata/client_logspoutClient.pem" 17 | clientKeyFileLocation = "./testdata/client_logspoutClient-key.pem" 18 | ) 19 | 20 | // helper function to create TLS config while handling error 21 | func createTestTLSConfig(t *testing.T) *tls.Config { 22 | testTLSConfig, err := createTLSConfig() 23 | if err != nil { 24 | t.Fatalf("we got an unexpected error while calling createTLSConfig: %s", err) 25 | } 26 | 27 | return testTLSConfig 28 | } 29 | 30 | // TestEmptyTrustStore should test the behavior of having 31 | // an empty TLS CA trust store. 32 | func TestEmptyTrustStore(t *testing.T) { 33 | os.Setenv(envDisableSystemRoots, "true") 34 | os.Unsetenv(envCaCerts) 35 | testTLSConfig := createTestTLSConfig(t) 36 | 37 | numOfTrustedCerts := len(testTLSConfig.RootCAs.Subjects()) 38 | if numOfTrustedCerts != 0 { 39 | t.Fatalf("expected 0 RootCAs but got: %d", numOfTrustedCerts) 40 | } 41 | } 42 | 43 | // TestSingleCustomCA should test the behavior of loading 44 | // a single custom CA certificate in to the trust store. 45 | func TestSingleCustomCA(t *testing.T) { 46 | os.Setenv(envDisableSystemRoots, "true") 47 | os.Setenv(envCaCerts, caRootCertFileLocation) 48 | testTLSConfig := createTestTLSConfig(t) 49 | 50 | // check if trust store has this cert 51 | if !bytes.Contains(testTLSConfig.RootCAs.Subjects()[0], []byte(caRootCertSubjectCN)) { 52 | t.Errorf("failed to load custom root CA into trust store: %s", caRootCertFileLocation) 53 | } 54 | } 55 | 56 | // TestMultipleCustomCAs should test the behavior of loading 57 | // multiple custom CA certificates in to the trust store. 58 | func TestMultipleCustomCAs(t *testing.T) { 59 | os.Setenv(envDisableSystemRoots, "true") 60 | os.Setenv(envCaCerts, caRootCertFileLocation+","+caIntCertFileLocation) 61 | testTLSConfig := createTestTLSConfig(t) 62 | 63 | // check that both certificates are in the trust store 64 | if !bytes.Contains(testTLSConfig.RootCAs.Subjects()[0], []byte(caRootCertSubjectCN)) { 65 | t.Errorf("failed to load custom root CA into trust store: %s", caRootCertFileLocation) 66 | } 67 | if !bytes.Contains(testTLSConfig.RootCAs.Subjects()[1], []byte(caIntCertSubjectCN)) { 68 | t.Errorf("failed to load custom intermediate CA into trust store: %s", caIntCertFileLocation) 69 | } 70 | } 71 | 72 | // TestSystemRootCAs should test that by default we load the system trust store 73 | func TestSystemRootCAs(t *testing.T) { 74 | // default behavior is none of these environment variables are set 75 | os.Unsetenv(envDisableSystemRoots) 76 | os.Unsetenv(envCaCerts) 77 | testTLSConfig := createTestTLSConfig(t) 78 | 79 | // its possible that the system does not have a trust store (minimal docker container for example) 80 | if len(testTLSConfig.RootCAs.Subjects()) < 1 { 81 | t.Errorf("after loading system trust store we still have 0. Do you have a system trust store?") 82 | } 83 | } 84 | 85 | // TestSystemRootCAsAndCustomCAs should test that we can load 86 | // both system CAs and custom CAs into trust store 87 | func TestSystemRootCAsAndCustomCAs(t *testing.T) { 88 | os.Unsetenv(envDisableSystemRoots) 89 | os.Unsetenv(envCaCerts) 90 | testTLSConfig := createTestTLSConfig(t) 91 | systemCACount := len(testTLSConfig.RootCAs.Subjects()) 92 | 93 | os.Setenv(envCaCerts, caRootCertFileLocation) 94 | testTLSConfig = createTestTLSConfig(t) 95 | currentCACount := len(testTLSConfig.RootCAs.Subjects()) 96 | 97 | if currentCACount != (systemCACount + 1) { 98 | t.Errorf("expected %d certs in trust store but got %d", systemCACount+1, currentCACount) 99 | } 100 | } 101 | 102 | // TestLoadingClientCertAndKey: should test the behavior of loading 103 | // a pem encoded client x509 certificate and private key 104 | func TestLoadingClientCertAndKey(t *testing.T) { 105 | os.Unsetenv(envDisableSystemRoots) 106 | os.Unsetenv(envCaCerts) 107 | os.Setenv(envClientCert, clientCertFileLocation) 108 | os.Setenv(envClientKey, clientKeyFileLocation) 109 | testTLSConfig := createTestTLSConfig(t) 110 | 111 | if len(testTLSConfig.Certificates) < 1 { 112 | t.Error("failed to load client certficate and key") 113 | } 114 | } 115 | 116 | // TestTLSHardening should test the behavior of enabling TLS hardening 117 | func TestTLSHardening(t *testing.T) { 118 | os.Unsetenv(envDisableSystemRoots) 119 | os.Unsetenv(envCaCerts) 120 | os.Unsetenv(envClientCert) 121 | os.Unsetenv(envClientKey) 122 | os.Setenv(envTLSHardening, "true") 123 | testTLSConfig := createTestTLSConfig(t) 124 | 125 | if testTLSConfig.MinVersion != hardenedMinVersion { 126 | t.Error("MinVersion is not set to expected value") 127 | } 128 | if testTLSConfig.InsecureSkipVerify { 129 | t.Error("InsecureSkipVerify is set to true when it should be false") 130 | } 131 | if len(testTLSConfig.CipherSuites) == 0 { 132 | t.Error("CipherSuites is not set") 133 | } 134 | if len(testTLSConfig.CurvePreferences) == 0 { 135 | t.Error("CurvePreferences is not set") 136 | } 137 | for i := range testTLSConfig.CipherSuites { 138 | if testTLSConfig.CipherSuites[i] != hardenedCiphers[i] { 139 | t.Error("discrepency found in CipherSuites") 140 | } 141 | } 142 | for i := range testTLSConfig.CurvePreferences { 143 | if testTLSConfig.CurvePreferences[i] != hardenedCurvePreferences[i] { 144 | t.Error("discrepency found in CurvePreferences") 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /transports/udp/udp.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/gliderlabs/logspout/adapters/raw" 7 | "github.com/gliderlabs/logspout/router" 8 | ) 9 | 10 | const ( 11 | // make configurable? 12 | writeBuffer = 1024 * 1024 13 | ) 14 | 15 | func init() { 16 | router.AdapterTransports.Register(new(udpTransport), "udp") 17 | // convenience adapters around raw adapter 18 | router.AdapterFactories.Register(rawUDPAdapter, "udp") 19 | } 20 | 21 | func rawUDPAdapter(route *router.Route) (router.LogAdapter, error) { 22 | route.Adapter = "raw+udp" 23 | return raw.NewRawAdapter(route) 24 | } 25 | 26 | type udpTransport int 27 | 28 | func (t *udpTransport) Dial(addr string, options map[string]string) (net.Conn, error) { 29 | raddr, err := net.ResolveUDPAddr("udp", addr) 30 | if err != nil { 31 | return nil, err 32 | } 33 | conn, err := net.DialUDP("udp", nil, raddr) 34 | if err != nil { 35 | return nil, err 36 | } 37 | // bump up the packet size for large log lines 38 | err = conn.SetWriteBuffer(writeBuffer) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return conn, nil 43 | } 44 | --------------------------------------------------------------------------------