├── .ci ├── test.bat └── test.sh ├── .github └── workflows │ ├── ci.yml │ ├── lint.yml │ └── test-reporter.yml ├── .gitignore ├── .go-version ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client ├── doc.go └── v2 │ ├── async.go │ ├── client.go │ ├── doc.go │ ├── opts.go │ └── sync.go ├── cmd ├── tst-lj │ └── main.go └── tst-send │ └── main.go ├── doc.go ├── go.mod ├── go.sum ├── lj └── lj.go ├── log └── log.go ├── protocol ├── doc.go ├── v1 │ └── protocol.go └── v2 │ └── protocol.go └── server ├── doc.go ├── internal ├── handler.go ├── server.go └── sig.go ├── mux.go ├── opts.go ├── server.go ├── v1 ├── doc.go ├── opts.go ├── reader.go ├── server.go └── writer.go └── v2 ├── doc.go ├── opts.go ├── reader.go ├── server.go └── writer.go /.ci/test.bat: -------------------------------------------------------------------------------- 1 | mkdir -p build 2 | SET OUT_FILE=build\output-report.out 3 | go test "./..." -v > %OUT_FILE% | type %OUT_FILE% 4 | 5 | go install github.com/jstemmer/go-junit-report/v2@latest 6 | go-junit-report > build\junit-%RUNNER_OS%.xml < %OUT_FILE% 7 | -------------------------------------------------------------------------------- /.ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Run the tests 5 | set +e 6 | export OUT_FILE="build/test-report.out" 7 | mkdir -p build 8 | go test "./..." -v 2>&1 | tee ${OUT_FILE} 9 | status=$? 10 | 11 | go get -v -u github.com/jstemmer/go-junit-report 12 | go-junit-report > "build/junit-${RUNNER_OS}.xml" < ${OUT_FILE} 13 | 14 | exit ${status} 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | tags: 8 | - 'v*' 9 | branches: 10 | - main 11 | pull_request: ~ 12 | 13 | permissions: 14 | contents: read 15 | 16 | ## Concurrency only allowed in the main branch. 17 | ## So old builds running for old commits within the same Pull Request are cancelled 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 21 | 22 | jobs: 23 | test: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | os: [macos-latest, ubuntu-latest, windows-latest] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: actions/setup-go@v4 33 | with: 34 | go-version-file: .go-version 35 | cache: true 36 | cache-dependency-path: '**/go.sum' 37 | 38 | - name: Build 39 | run: go build 40 | 41 | - name: Test 42 | run: | 43 | if [ "$RUNNER_OS" == "Windows" ]; then 44 | .ci/test.bat 45 | else 46 | .ci/test.sh 47 | fi 48 | shell: bash 49 | 50 | - name: Store test results 51 | if: success() || failure() 52 | continue-on-error: true 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: test-results-${{ matrix.os }} 56 | path: "build/junit-*.xml" 57 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: read 14 | 15 | ## Concurrency only allowed in the main branch. 16 | ## So old builds running for old commits within the same Pull Request are cancelled 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 20 | 21 | jobs: 22 | golangci: 23 | name: golangci-lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - uses: actions/setup-go@v4 29 | with: 30 | go-version-file: .go-version 31 | 32 | - name: check go.mod 33 | run: | 34 | go mod tidy && git diff --exit-code 35 | go mod verify 36 | 37 | - name: golangci-lint 38 | uses: golangci/golangci-lint-action@v3 39 | with: 40 | version: latest 41 | only-new-issues: true 42 | 43 | - name: go-licenser 44 | run: |- 45 | go get github.com/elastic/go-licenser 46 | go run github.com/elastic/go-licenser -d 47 | -------------------------------------------------------------------------------- /.github/workflows/test-reporter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Workflow to process the JUnit test results and add a report to the checks. 3 | name: test-reporter 4 | 5 | on: 6 | workflow_run: 7 | workflows: [ci] 8 | types: [completed] 9 | 10 | permissions: 11 | contents: read 12 | actions: read 13 | checks: write 14 | 15 | jobs: 16 | report: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: elastic/oblt-actions/test-report@v1 20 | with: 21 | artifact: /test-results(.*)/ # artifact name 22 | name: 'Test Report $1' # Name of the check run which will be created 23 | path: "**/*.xml" # Path to test results (inside artifact .zip) 24 | reporter: java-junit # Format of test results 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | go-lumber 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.19 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | run: 4 | issues-exit-code: 1 5 | modules-download-mode: readonly 6 | 7 | linters: 8 | disable-all: true 9 | enable: 10 | - deadcode 11 | - errcheck 12 | - gosimple 13 | - govet 14 | - ineffassign 15 | - staticcheck 16 | - structcheck 17 | - stylecheck 18 | - typecheck 19 | - unused 20 | - varcheck 21 | - depguard 22 | - errorlint 23 | - gofumpt 24 | - goimports 25 | - godox 26 | - goheader 27 | - misspell 28 | - prealloc 29 | - unconvert 30 | - revive 31 | fast: false 32 | 33 | linters-settings: 34 | goimports: 35 | local-prefixes: github.com/elastic/go-lumber 36 | gofumpt: 37 | extra-rules: true 38 | revive: 39 | enable-all-rules: false 40 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md 41 | rules: 42 | - name: bare-return 43 | - name: call-to-gc 44 | - name: confusing-results 45 | - name: constant-logical-expr 46 | - name: context-as-argument 47 | - name: deep-exit 48 | - name: defer 49 | - name: duplicated-imports 50 | - name: early-return 51 | - name: empty-block 52 | - name: error-strings 53 | - name: errorf 54 | - name: exported 55 | arguments: 56 | - checkPrivateReceivers 57 | - name: imports-blacklist 58 | arguments: 59 | - github.com/pkg/errors 60 | - name: increment-decrement 61 | - name: range 62 | - name: range-val-address 63 | - name: range-val-in-closure 64 | - name: receiver-naming 65 | - name: struct-tag 66 | - name: time-naming 67 | - name: unconditional-recursion 68 | - name: unexported-naming 69 | - name: unexported-return 70 | - name: unnecessary-stmt 71 | - name: unreachable-code 72 | - name: unused-parameter 73 | - name: unused-receiver 74 | - name: var-declaration 75 | - name: waitgroup-by-value 76 | stylecheck: 77 | checks: 78 | - all 79 | 80 | issues: 81 | include: 82 | # If you're going to write a comment follow the conventions. 83 | # https://go.dev/doc/effective_go#commentary. 84 | # comment on exported (.+) should be of the form "(.+)..." 85 | - EXC0014 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ### Added 8 | 9 | - Add connection metadata to `lj.Batch`. [#29](https://github.com/elastic/go-lumber/pull/29) 10 | 11 | ### Changed 12 | 13 | - Require Go 1.17 to use module. [#28](https://github.com/elastic/go-lumber/pull/28) 14 | 15 | ### Deprecated 16 | 17 | ### Removed 18 | 19 | ### Fixed 20 | 21 | ## [0.1.1] 22 | 23 | ### Fixed 24 | 25 | - Fix goroutine and file descriptor leak if an error occurs during connection startup. [#15](https://github.com/elastic/go-lumber/pull/15) 26 | 27 | [Unreleased]: https://github.com/elastic/go-lumber/compare/v0.1.1...HEAD 28 | [0.1.1]: https://github.com/elastic/go-concert/compare/v0.1.0...v0.1.1 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-lumber 2 | [![ci](https://github.com/elastic/go-lumber/actions/workflows/ci.yml/badge.svg)](https://github.com/elastic/go-lumber/actions/workflows/ci.yml) 3 | [![Go Report 4 | Card](https://goreportcard.com/badge/github.com/elastic/go-lumber)](https://goreportcard.com/report/github.com/elastic/go-lumber) 5 | [![Contributors](https://img.shields.io/github/contributors/elastic/go-lumber.svg)](https://github.com/elastic/go-lumber/graphs/contributors) 6 | [![GitHub release](https://img.shields.io/github/release/elastic/go-lumber.svg?label=changelog)](https://github.com/elastic/go-lumber/releases/latest) 7 | 8 | Lumberjack protocol client and server implementations for go. 9 | 10 | ## Example Server 11 | 12 | There is an example server in [cmd/tst-lj](cmd/tst-lj/main.go). It will accept 13 | connections and log when it receives batches of events. 14 | 15 | ``` 16 | # Install to $GOPATH/bin. 17 | go install github.com/elastic/go-lumber/cmd/tst-lj@latest 18 | 19 | # Start server. 20 | tst-lj -bind=localhost:5044 -v2 21 | 2022/08/14 00:13:54 Server config: server.options{timeout:30000000000, keepalive:3000000000, decoder:(server.jsonDecoder)(0x100d88e80), tls:(*tls.Config)(nil), v1:false, v2:true, ch:(chan *lj.Batch)(nil)} 22 | 2022/08/14 00:13:54 tcp server up 23 | ``` 24 | -------------------------------------------------------------------------------- /client/doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package client implementing lumberjack clients. 19 | package client 20 | -------------------------------------------------------------------------------- /client/v2/async.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "io" 22 | "net" 23 | "sync" 24 | ) 25 | 26 | // AsyncClient asynchronously publishes events to lumberjack endpoint. On ACK a 27 | // provided callback function will be called. The number of in-flight publish 28 | // requests is configurable but limited. Once the limit has been reached, the 29 | // client will block publish requests until the lumberjack server did ACK some 30 | // queued publish requests. 31 | type AsyncClient struct { 32 | cl *Client 33 | 34 | inflight int 35 | ch chan ackMessage 36 | wg sync.WaitGroup 37 | } 38 | 39 | type ackMessage struct { 40 | cb AsyncSendCallback 41 | seq uint32 42 | err error 43 | } 44 | 45 | // AsyncSendCallback callback function. Upon completion seq contains the last 46 | // ACKed event's index. The count starts with 1. The err argument contains the latest 47 | // error encountered by lumberjack client. 48 | // 49 | // Note: The callback MUST not block. In case callback is trying to republish 50 | // not ACKed events, care must be taken not to deadlock the AsyncClient when calling 51 | // Send. 52 | type AsyncSendCallback func(seq uint32, err error) 53 | 54 | // NewAsyncClientWith creates a new AsyncClient from low-level lumberjack v2 Client. 55 | // The inflight argument sets number of active publish requests. 56 | func NewAsyncClientWith(cl *Client, inflight int) (*AsyncClient, error) { 57 | c := &AsyncClient{ 58 | cl: cl, 59 | inflight: inflight, 60 | } 61 | 62 | c.startACK() 63 | return c, nil 64 | } 65 | 66 | // NewAsyncClientWithConn creates a new AsyncClient from an active connection. 67 | func NewAsyncClientWithConn(c net.Conn, inflight int, opts ...Option) (*AsyncClient, error) { 68 | cl, err := NewWithConn(c, opts...) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return NewAsyncClientWith(cl, inflight) 73 | } 74 | 75 | // AsyncDial connects to lumberjack server and returns new AsyncClient. On error 76 | // no AsyncClient is being created. 77 | func AsyncDial(address string, inflight int, opts ...Option) (*AsyncClient, error) { 78 | cl, err := Dial(address, opts...) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return NewAsyncClientWith(cl, inflight) 83 | } 84 | 85 | // AsyncDialWith uses provided dialer to connect to lumberjack server. On error 86 | // no AsyncClient is being returned. 87 | func AsyncDialWith( 88 | dial func(network, address string) (net.Conn, error), 89 | address string, 90 | inflight int, 91 | opts ...Option, 92 | ) (*AsyncClient, error) { 93 | cl, err := DialWith(dial, address, opts...) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return NewAsyncClientWith(cl, inflight) 98 | } 99 | 100 | // Close closes the client, so no new events can be published anymore. The 101 | // underlying network connection will be closed too. Returns an error if 102 | // underlying net.Conn errors on Close. 103 | // 104 | // All inflight requests will be cancelled, returning EOF if no other error has 105 | // been encountered due to underlying network connection being closed. 106 | // 107 | // The client gives no guarantees regarding published events. There is a chance 108 | // events will be processed by server, even though connection has been closed. 109 | func (c *AsyncClient) Close() error { 110 | err := c.cl.Close() 111 | c.stopACK() 112 | return err 113 | } 114 | 115 | // Send publishes a new batch of events by JSON-encoding given batch. 116 | // Send blocks if maximum number of allowed asynchronous calls is still active. 117 | // Upon completion cb will be called with last ACKed index into active batch. 118 | // Returns error if communication or serialization to JSON failed. 119 | func (c *AsyncClient) Send(cb AsyncSendCallback, data []interface{}) error { 120 | if err := c.cl.Send(data); err != nil { 121 | c.ch <- ackMessage{ 122 | seq: 0, 123 | cb: cb, 124 | err: err, 125 | } 126 | return err 127 | } 128 | 129 | c.ch <- ackMessage{ 130 | seq: uint32(len(data)), 131 | cb: cb, 132 | err: nil, 133 | } 134 | return nil 135 | } 136 | 137 | func (c *AsyncClient) startACK() { 138 | c.ch = make(chan ackMessage, c.inflight) 139 | c.wg.Add(1) 140 | go c.ackLoop() 141 | } 142 | 143 | func (c *AsyncClient) stopACK() { 144 | close(c.ch) 145 | c.wg.Wait() 146 | } 147 | 148 | func (c *AsyncClient) ackLoop() { 149 | var seq uint32 150 | var err error 151 | 152 | // drain ack queue on error/exit 153 | defer func() { 154 | if err == nil { 155 | err = io.EOF 156 | } 157 | for msg := range c.ch { 158 | if msg.err != nil { 159 | err = msg.err 160 | } 161 | msg.cb(0, err) 162 | } 163 | }() 164 | defer c.wg.Done() 165 | 166 | for msg := range c.ch { 167 | if msg.err != nil { 168 | err = msg.err 169 | msg.cb(msg.seq, msg.err) 170 | return 171 | } 172 | 173 | seq, err = c.cl.AwaitACK(msg.seq) 174 | msg.cb(seq, err) 175 | if err != nil { 176 | c.cl.Close() 177 | return 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /client/v2/client.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "bytes" 22 | "encoding/binary" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "net" 27 | "time" 28 | 29 | "github.com/klauspost/compress/zlib" 30 | 31 | protocol "github.com/elastic/go-lumber/protocol/v2" 32 | ) 33 | 34 | // Client implements the low-level lumberjack wire protocol. SyncClient and 35 | // AsyncClient should be used for publishing events to lumberjack endpoint. 36 | type Client struct { 37 | conn net.Conn 38 | wb *bytes.Buffer 39 | 40 | opts options 41 | } 42 | 43 | var ( 44 | codeWindowSize = []byte{protocol.CodeVersion, protocol.CodeWindowSize} 45 | codeCompressed = []byte{protocol.CodeVersion, protocol.CodeCompressed} 46 | codeJSONDataFrame = []byte{protocol.CodeVersion, protocol.CodeJSONDataFrame} 47 | 48 | empty4 = []byte{0, 0, 0, 0} 49 | ) 50 | 51 | // ErrProtocolError is returned if a protocol error was detected in the 52 | // conversation with lumberjack server. 53 | var ErrProtocolError = errors.New("lumberjack protocol error") 54 | 55 | // NewWithConn create a new lumberjack client with an existing and active 56 | // connection. 57 | func NewWithConn(c net.Conn, opts ...Option) (*Client, error) { 58 | o, err := applyOptions(opts) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &Client{ 63 | conn: c, 64 | wb: bytes.NewBuffer(nil), 65 | opts: o, 66 | }, nil 67 | } 68 | 69 | // Dial connects to the lumberjack server and returns new Client. 70 | // Returns an error if connection attempt fails. 71 | func Dial(address string, opts ...Option) (*Client, error) { 72 | o, err := applyOptions(opts) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | dialer := net.Dialer{Timeout: o.timeout} 78 | return DialWith(dialer.Dial, address, opts...) 79 | } 80 | 81 | // DialWith uses provided dialer to connect to lumberjack server returning a 82 | // new Client. Returns error if connection attempt fails. 83 | func DialWith( 84 | dial func(network, address string) (net.Conn, error), 85 | address string, 86 | opts ...Option, 87 | ) (*Client, error) { 88 | c, err := dial("tcp", address) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | client, err := NewWithConn(c, opts...) 94 | if err != nil { 95 | _ = c.Close() // ignore error 96 | return nil, err 97 | } 98 | return client, nil 99 | } 100 | 101 | // Close closes underlying network connection 102 | func (c *Client) Close() error { 103 | return c.conn.Close() 104 | } 105 | 106 | // Send attempts to JSON-encode and send all events without waiting for ACK. 107 | // Returns error if sending or serialization fails. 108 | func (c *Client) Send(data []interface{}) error { 109 | if len(data) == 0 { 110 | return nil 111 | } 112 | 113 | // 1. create window message 114 | c.wb.Reset() 115 | _, _ = c.wb.Write(codeWindowSize) 116 | writeUint32(c.wb, uint32(len(data))) 117 | 118 | // 2. serialize data (payload) 119 | if c.opts.compressLvl > 0 { 120 | // Compressed Data Frame: 121 | // version: uint8 = '2' 122 | // code: uint8 = 'C' 123 | // payloadSz: uint32 124 | // payload: compressed payload 125 | 126 | _, _ = c.wb.Write(codeCompressed) // write compressed header 127 | 128 | offSz := c.wb.Len() 129 | _, _ = c.wb.Write(empty4) 130 | offPayload := c.wb.Len() 131 | 132 | // compress payload 133 | w, err := zlib.NewWriterLevel(c.wb, c.opts.compressLvl) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | if err := c.serialize(w, data); err != nil { 139 | return err 140 | } 141 | 142 | if err := w.Close(); err != nil { 143 | return err 144 | } 145 | 146 | // write compress header 147 | payloadSz := c.wb.Len() - offPayload 148 | binary.BigEndian.PutUint32(c.wb.Bytes()[offSz:], uint32(payloadSz)) 149 | } else { 150 | if err := c.serialize(c.wb, data); err != nil { 151 | return err 152 | } 153 | } 154 | 155 | // 3. send buffer 156 | if err := c.setWriteDeadline(); err != nil { 157 | return err 158 | } 159 | payload := c.wb.Bytes() 160 | for len(payload) > 0 { 161 | n, err := c.conn.Write(payload) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | payload = payload[n:] 167 | } 168 | 169 | return nil 170 | } 171 | 172 | // ReceiveACK awaits and reads next ACK response or error. Note: Server might 173 | // send partial ACK, in which case client must continue reading ACKs until last send 174 | // window size is matched. Use AwaitACK when waiting for a known sequence number. 175 | func (c *Client) ReceiveACK() (uint32, error) { 176 | if err := c.setReadDeadline(); err != nil { 177 | return 0, err 178 | } 179 | 180 | var msg [6]byte 181 | ackBytes := 0 182 | for ackBytes < 6 { 183 | n, err := c.conn.Read(msg[ackBytes:]) 184 | if err != nil { 185 | return 0, err 186 | } 187 | ackBytes += n 188 | } 189 | 190 | // validate response 191 | isACK := msg[0] == protocol.CodeVersion && msg[1] == protocol.CodeACK 192 | if !isACK { 193 | return 0, ErrProtocolError 194 | } 195 | 196 | seq := binary.BigEndian.Uint32(msg[2:]) 197 | return seq, nil 198 | } 199 | 200 | // AwaitACK waits for count elements being ACKed. Returns last known ACK on error. 201 | func (c *Client) AwaitACK(count uint32) (uint32, error) { 202 | var ackSeq uint32 203 | var err error 204 | 205 | // read until all ACKs 206 | for ackSeq < count { 207 | ackSeq, err = c.ReceiveACK() 208 | if err != nil { 209 | return ackSeq, err 210 | } 211 | } 212 | 213 | if ackSeq > count { 214 | return count, fmt.Errorf( 215 | "invalid sequence number received (seq=%v, expected=%v)", ackSeq, count) 216 | } 217 | return ackSeq, nil 218 | } 219 | 220 | func (c *Client) serialize(out io.Writer, data []interface{}) error { 221 | for i, d := range data { 222 | b, err := c.opts.encoder(d) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | // Write JSON Data Frame: 228 | // version: uint8 = '2' 229 | // code: uint8 = 'J' 230 | // seq: uint32 231 | // payloadLen (bytes): uint32 232 | // payload: JSON document 233 | 234 | _, _ = out.Write(codeJSONDataFrame) 235 | writeUint32(out, uint32(i)+1) 236 | writeUint32(out, uint32(len(b))) 237 | _, _ = out.Write(b) 238 | } 239 | return nil 240 | } 241 | 242 | func (c *Client) setWriteDeadline() error { 243 | return c.conn.SetWriteDeadline(time.Now().Add(c.opts.timeout)) 244 | } 245 | 246 | func (c *Client) setReadDeadline() error { 247 | return c.conn.SetReadDeadline(time.Now().Add(c.opts.timeout)) 248 | } 249 | 250 | func writeUint32(out io.Writer, v uint32) { 251 | _ = binary.Write(out, binary.BigEndian, v) 252 | } 253 | -------------------------------------------------------------------------------- /client/v2/doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package v2 implements clients supporting lumberjack protocol version 2. 19 | // 20 | // This package provides the low level `Client` handling the wire-format only, 21 | // plus `SyncClient` and AsyncClient. SyncClient and AsyncClient do provide 22 | // protocol compliant communication and error handling with lumberjack server. 23 | package v2 24 | -------------------------------------------------------------------------------- /client/v2/opts.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | "time" 24 | ) 25 | 26 | // Option type to be passed to New/Dial functions. 27 | type Option func(*options) error 28 | 29 | type options struct { 30 | timeout time.Duration 31 | encoder jsonEncoder 32 | compressLvl int 33 | } 34 | 35 | type jsonEncoder func(interface{}) ([]byte, error) 36 | 37 | // JSONEncoder client option configuring the encoder used to convert events 38 | // to json. The default is `json.Marshal`. 39 | func JSONEncoder(encoder func(interface{}) ([]byte, error)) Option { 40 | return func(opt *options) error { 41 | opt.encoder = encoder 42 | return nil 43 | } 44 | } 45 | 46 | // Timeout client option configuring read/write timeout. 47 | func Timeout(to time.Duration) Option { 48 | return func(opt *options) error { 49 | if to < 0 { 50 | return errors.New("timeouts must not be negative") 51 | } 52 | opt.timeout = to 53 | return nil 54 | } 55 | } 56 | 57 | // CompressionLevel client option setting the gzip compression level (0 to 9). 58 | func CompressionLevel(l int) Option { 59 | return func(opt *options) error { 60 | if !(0 <= l && l <= 9) { 61 | return errors.New("compression level must be within 0 and 9") 62 | } 63 | opt.compressLvl = l 64 | return nil 65 | } 66 | } 67 | 68 | func applyOptions(opts []Option) (options, error) { 69 | o := options{ 70 | encoder: json.Marshal, 71 | timeout: 30 * time.Second, 72 | } 73 | 74 | for _, opt := range opts { 75 | if err := opt(&o); err != nil { 76 | return o, err 77 | } 78 | } 79 | return o, nil 80 | } 81 | -------------------------------------------------------------------------------- /client/v2/sync.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import "net" 21 | 22 | // SyncClient synchronously publishes events to lumberjack endpoint waiting for 23 | // ACK before allowing another send request. The client is not thread-safe. 24 | type SyncClient struct { 25 | cl *Client 26 | } 27 | 28 | // NewSyncClientWith creates a new SyncClient from low-level lumberjack v2 Client. 29 | func NewSyncClientWith(c *Client) (*SyncClient, error) { 30 | return &SyncClient{c}, nil 31 | } 32 | 33 | // NewSyncClientWithConn creates a new SyncClient from an active connection. 34 | func NewSyncClientWithConn(c net.Conn, opts ...Option) (*SyncClient, error) { 35 | cl, err := NewWithConn(c, opts...) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return NewSyncClientWith(cl) 40 | } 41 | 42 | // SyncDial connects to lumberjack server and returns new SyncClient. On error 43 | // no SyncClient is being created. 44 | func SyncDial(address string, opts ...Option) (*SyncClient, error) { 45 | cl, err := Dial(address, opts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return NewSyncClientWith(cl) 50 | } 51 | 52 | // SyncDialWith uses provided dialer to connect to lumberjack server. On error 53 | // no SyncClient is being returned. 54 | func SyncDialWith( 55 | dial func(network, address string) (net.Conn, error), 56 | address string, 57 | opts ...Option, 58 | ) (*SyncClient, error) { 59 | cl, err := DialWith(dial, address, opts...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return NewSyncClientWith(cl) 64 | } 65 | 66 | // Close closes the client, so no new events can be published anymore. The 67 | // underlying network connection will be closed too. Returns an error if 68 | // underlying net.Conn errors on Close. 69 | func (c *SyncClient) Close() error { 70 | return c.cl.Close() 71 | } 72 | 73 | // Send publishes a new batch of events by JSON-encoding given batch. 74 | // Send blocks until the complete batch has been ACKed by lumberjack server or 75 | // some error happened. 76 | func (c *SyncClient) Send(data []interface{}) (int, error) { 77 | if err := c.cl.Send(data); err != nil { 78 | return 0, err 79 | } 80 | 81 | seq, err := c.cl.AwaitACK(uint32(len(data))) 82 | return int(seq), err 83 | } 84 | -------------------------------------------------------------------------------- /cmd/tst-lj/main.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Lumberjack server test tool. 19 | // 20 | // Create lumberjack server endpoint ACKing all received batches only. The 21 | // server supports all lumberjack protocol versions, which must be explicitly enabled 22 | // from command line. For printing list of known command line flags run: 23 | // 24 | // tst-lj -h 25 | package main 26 | 27 | import ( 28 | "flag" 29 | "fmt" 30 | "log" 31 | "os" 32 | "os/signal" 33 | "syscall" 34 | "time" 35 | 36 | "github.com/elastic/go-lumber/lj" 37 | "github.com/elastic/go-lumber/server" 38 | ) 39 | 40 | type rateLimiter struct { 41 | ticker *time.Ticker 42 | ch chan time.Time 43 | } 44 | 45 | func main() { 46 | bind := flag.String("bind", ":5044", "[host]:port to listen on") 47 | v1 := flag.Bool("v1", false, "Enable protocol version v1") 48 | v2 := flag.Bool("v2", false, "Enable protocol version v2") 49 | limit := flag.Int("rate", 0, "max batch ack rate") 50 | detailed := flag.Bool("d", false, "detailed: print log message per event") 51 | flag.Parse() 52 | 53 | s, err := server.ListenAndServe(*bind, 54 | server.V1(*v1), 55 | server.V2(*v2)) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | log.Println("tcp server up") 61 | 62 | var rl *rateLimiter 63 | if *limit > 0 { 64 | rl = newRateLimiter(*limit, (*limit)*2, time.Second) 65 | } 66 | 67 | sig := make(chan os.Signal, 1) 68 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 69 | go func() { 70 | <-sig 71 | if rl != nil { 72 | rl.Stop() 73 | } 74 | _ = s.Close() 75 | os.Exit(0) 76 | }() 77 | 78 | printLog := func(batch *lj.Batch) bool { 79 | log.Printf("Received batch of %v events from %s", len(batch.Events), batch.RemoteAddr) 80 | return true 81 | } 82 | 83 | switch { 84 | case rl == nil && *detailed: 85 | printLog = func(batch *lj.Batch) bool { 86 | for range batch.Events { 87 | log.Println("Received event") 88 | } 89 | return true 90 | } 91 | 92 | case rl != nil && *detailed: 93 | printLog = func(batch *lj.Batch) bool { 94 | for range batch.Events { 95 | if !rl.Wait() { 96 | return false 97 | } 98 | log.Println("Received event") 99 | } 100 | return true 101 | } 102 | 103 | case rl != nil && !(*detailed): 104 | printLog = func(batch *lj.Batch) bool { 105 | for range batch.Events { 106 | if !rl.Wait() { 107 | return false 108 | } 109 | } 110 | 111 | log.Printf("Received batch of %v events\n", len(batch.Events)) 112 | return true 113 | } 114 | } 115 | 116 | for batch := range s.ReceiveChan() { 117 | if !printLog(batch) { 118 | break 119 | } 120 | batch.ACK() 121 | } 122 | } 123 | 124 | func newRateLimiter(limit, burstLimit int, unit time.Duration) *rateLimiter { 125 | interval := time.Duration(uint64(unit) / uint64(limit)) 126 | fmt.Println("rate limiter interval:", interval) 127 | ticker := time.NewTicker(interval) 128 | ch := make(chan time.Time, burstLimit) 129 | r := &rateLimiter{ticker: ticker, ch: ch} 130 | 131 | go func() { 132 | defer close(ch) 133 | for t := range ticker.C { 134 | ch <- t 135 | } 136 | }() 137 | 138 | return r 139 | } 140 | 141 | func (r *rateLimiter) Stop() { 142 | r.ticker.Stop() 143 | } 144 | 145 | func (r *rateLimiter) Wait() bool { 146 | _, ok := <-r.ch 147 | return ok 148 | } 149 | -------------------------------------------------------------------------------- /cmd/tst-send/main.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Test client pushing precomputed batch of events. 19 | // 20 | // For list of known command line flags run: 21 | // 22 | // tst-send -h 23 | package main 24 | 25 | import ( 26 | "expvar" 27 | "flag" 28 | "log" 29 | "math/rand" 30 | "net/http" 31 | "os" 32 | "strings" 33 | "time" 34 | 35 | v2 "github.com/elastic/go-lumber/client/v2" 36 | ) 37 | 38 | func main() { 39 | connect := flag.String("c", "localhost:5044", "Remote address") 40 | compress := flag.Int("compress", 3, "Compression level (0-9)") 41 | timeout := flag.Duration("timeout", 30*time.Second, "Connection timeouts") 42 | batchSize := flag.Int64("batch", 2048, "Batch size") 43 | pipelined := flag.Int("pipeline", 0, "enabled pipeline mode with number of batches kept in pipeline") 44 | httpprof := flag.String("httpprof", ":6060", "HTTP profiling server address") 45 | flag.Parse() 46 | 47 | stat := expvar.NewInt("ACKed") 48 | 49 | batch := make([]interface{}, *batchSize) 50 | for i := range batch { 51 | batch[i] = makeEvent() 52 | } 53 | 54 | go func() { 55 | log.Printf("Listening: %v\n", *httpprof) 56 | if err := http.ListenAndServe(*httpprof, nil); err != nil { 57 | log.Fatal("Failed to start HTTP server:", err) 58 | } 59 | }() 60 | 61 | log.Printf("connect to: %v", *connect) 62 | if *pipelined == 0 { 63 | cl, err := v2.SyncDial(*connect, 64 | v2.CompressionLevel(*compress), 65 | v2.Timeout(*timeout)) 66 | if err != nil { 67 | log.Println(err) 68 | os.Exit(1) 69 | } 70 | 71 | for { 72 | _, err := cl.Send(batch) 73 | if err != nil { 74 | log.Println(err) 75 | return 76 | } 77 | 78 | stat.Add(*batchSize) 79 | } 80 | } else { 81 | cl, err := v2.AsyncDial(*connect, 82 | *pipelined, 83 | v2.CompressionLevel(*compress), 84 | v2.Timeout(*timeout)) 85 | if err != nil { 86 | log.Println(err) 87 | os.Exit(1) 88 | } 89 | 90 | for { 91 | cb := func(_ uint32, err error) { 92 | if err != nil { 93 | log.Println(err) 94 | return 95 | } 96 | 97 | stat.Add(*batchSize) 98 | } 99 | 100 | err := cl.Send(cb, batch) 101 | if err != nil { 102 | log.Println(err) 103 | return 104 | } 105 | } 106 | } 107 | } 108 | 109 | var text = strings.Split(`Lorem ipsum dolor sit amet, consetetur sadipscing elitr, 110 | sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, 111 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 112 | clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 113 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 114 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 115 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, 116 | no sea takimata sanctus est Lorem ipsum dolor sit amet.`, "\n") 117 | 118 | func makeEvent() interface{} { 119 | line := text[rand.Intn(len(text))] 120 | return map[string]interface{}{ 121 | "@timestamp": time.Now(), 122 | "type": "filebeat", 123 | "message": line, 124 | "offset": 1000, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package lumber provides lumberjack client and server implementations. 19 | package lumber 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elastic/go-lumber 2 | 3 | go 1.17 4 | 5 | require github.com/klauspost/compress v1.15.9 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= 2 | github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 3 | -------------------------------------------------------------------------------- /lj/lj.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package lj implements common lumberjack types and functions. 19 | package lj 20 | 21 | import ( 22 | "crypto/tls" 23 | ) 24 | 25 | // Batch is an ACK-able batch of events that has been received by lumberjack 26 | // server implementations. Batches must be ACKed for the server 27 | // implementations returning an ACK to its clients. 28 | type Batch struct { 29 | ack chan struct{} 30 | TLS *tls.ConnectionState // TLS connection metadata. Nil for non-TLS connections. 31 | RemoteAddr string // Source address of the connection. 32 | Events []interface{} 33 | } 34 | 35 | // NewBatch creates a new ACK-able batch. 36 | func NewBatch(events []interface{}) *Batch { 37 | return NewBatchWithSourceMetadata(events, "", nil) 38 | } 39 | 40 | // NewBatchWithSourceMetadata creates a new ACK-able batch with metadata about 41 | // the source of the batch. remoteAddr is the origin address (ip and port). 42 | // tlsState is the TLS connection metadata when the server uses TLS, otherwise 43 | // it should be nil. 44 | func NewBatchWithSourceMetadata(events []interface{}, remoteAddr string, tlsState *tls.ConnectionState) *Batch { 45 | return &Batch{ 46 | ack: make(chan struct{}), 47 | TLS: tlsState, 48 | RemoteAddr: remoteAddr, 49 | Events: events, 50 | } 51 | } 52 | 53 | // ACK acknowledges a batch initiating propagation of ACK to clients. 54 | func (b *Batch) ACK() { 55 | close(b.ack) 56 | } 57 | 58 | // Await returns a channel for waiting for a batch to be ACKed. 59 | func (b *Batch) Await() <-chan struct{} { 60 | return b.ack 61 | } 62 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package log provides logging functionality used in go-lumber. 19 | // 20 | // The log package provides replaceable logging for use from within go-lumber. 21 | // Overwrite Logging variable with custom Logging implementation for integrating 22 | // go-lumber logging with applications logging strategy. 23 | package log 24 | 25 | import "log" 26 | 27 | // Logging interface custom loggers must implement. 28 | type Logging interface { 29 | Printf(string, ...interface{}) 30 | Println(...interface{}) 31 | Print(...interface{}) 32 | } 33 | 34 | type defaultLogger struct{} 35 | 36 | // Logger provides the global logger used by go-lumber. 37 | var Logger Logging = defaultLogger{} 38 | 39 | // Printf calls Logger.Printf to print to the standard logger. Arguments are 40 | // handled in the manner of fmt.Printf. 41 | func Printf(format string, args ...interface{}) { 42 | Logger.Printf(format, args...) 43 | } 44 | 45 | // Println calls Logger.Println to write to the standard logger. Arguments are 46 | // handled in the manner of fmt.Println. 47 | func Println(args ...interface{}) { 48 | Logger.Println(args...) 49 | } 50 | 51 | // Print calls Logger.Print to write to the standard logger. Arguments are 52 | // handled in the manner of fmt.Print. 53 | func Print(args ...interface{}) { 54 | Logger.Print(args...) 55 | } 56 | 57 | func (defaultLogger) Printf(format string, args ...interface{}) { 58 | log.Printf(format, args...) 59 | } 60 | 61 | func (defaultLogger) Println(args ...interface{}) { 62 | log.Println(args...) 63 | } 64 | 65 | func (defaultLogger) Print(args ...interface{}) { 66 | log.Print(args...) 67 | } 68 | -------------------------------------------------------------------------------- /protocol/doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package protocol provides common protocol definitions used by client and 19 | // server implementations. 20 | package protocol 21 | -------------------------------------------------------------------------------- /protocol/v1/protocol.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package v1 provides common lumberjack protocol version 1 definitions. 19 | package v1 20 | 21 | // Version declares the protocol revision supported by this package. 22 | const Version = 1 23 | 24 | // Lumberjack protocol version 1 message types. 25 | const ( 26 | CodeVersion byte = '1' 27 | 28 | CodeWindowSize byte = 'W' 29 | CodeDataFrame byte = 'D' 30 | CodeCompressed byte = 'C' 31 | CodeACK byte = 'A' 32 | ) 33 | -------------------------------------------------------------------------------- /protocol/v2/protocol.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package v2 provides common lumberjack protocol version 2 definitions. 19 | package v2 20 | 21 | // Version declares the protocol revision supported by this package. 22 | const Version = 2 23 | 24 | // Lumberjack protocol version 2 message types. 25 | const ( 26 | CodeVersion byte = '2' 27 | 28 | CodeWindowSize byte = 'W' 29 | CodeJSONDataFrame byte = 'J' 30 | CodeCompressed byte = 'C' 31 | CodeACK byte = 'A' 32 | ) 33 | -------------------------------------------------------------------------------- /server/doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package server provides lumberjack server implementations. The Server 19 | // implementation supports multiplexing different protocol versions. 20 | package server 21 | -------------------------------------------------------------------------------- /server/internal/handler.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package internal 19 | 20 | import ( 21 | "net" 22 | "sync" 23 | "time" 24 | 25 | "github.com/elastic/go-lumber/lj" 26 | "github.com/elastic/go-lumber/log" 27 | ) 28 | 29 | type defaultHandler struct { 30 | cb Eventer 31 | client net.Conn 32 | reader BatchReader 33 | writer ACKWriter 34 | keepalive time.Duration 35 | 36 | signal chan struct{} 37 | ch chan *lj.Batch 38 | 39 | stopGuard sync.Once 40 | } 41 | 42 | type BatchReader interface { 43 | ReadBatch() (*lj.Batch, error) 44 | } 45 | 46 | type ACKWriter interface { 47 | Keepalive(int) error 48 | ACK(int) error 49 | } 50 | 51 | type ProtocolFactory func(conn net.Conn) (BatchReader, ACKWriter, error) 52 | 53 | func DefaultHandler( 54 | keepalive time.Duration, 55 | mk ProtocolFactory, 56 | ) HandlerFactory { 57 | return func(cb Eventer, client net.Conn) (Handler, error) { 58 | r, w, err := mk(client) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return &defaultHandler{ 64 | cb: cb, 65 | client: client, 66 | reader: r, 67 | writer: w, 68 | keepalive: keepalive, 69 | signal: make(chan struct{}), 70 | ch: make(chan *lj.Batch), 71 | }, nil 72 | } 73 | } 74 | 75 | func (h *defaultHandler) Run() { 76 | // start async routine for returning ACKs to client. 77 | // Sends ACK of 0 every 'keepalive' seconds to signal 78 | // client the batch still being in pipeline 79 | go h.ackLoop() 80 | if err := h.handle(); err != nil { 81 | log.Println(err) 82 | } 83 | } 84 | 85 | func (h *defaultHandler) Stop() { 86 | h.stopGuard.Do(func() { 87 | close(h.signal) 88 | _ = h.client.Close() 89 | }) 90 | } 91 | 92 | func (h *defaultHandler) handle() error { 93 | log.Printf("Start client handler") 94 | defer log.Printf("client handler stopped") 95 | defer close(h.ch) 96 | defer h.Stop() 97 | 98 | for { 99 | // 1. read data into batch 100 | b, err := h.reader.ReadBatch() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | // read next batch if empty batch has been received 106 | if b == nil { 107 | continue 108 | } 109 | 110 | // 2. push batch to ACK queue 111 | select { 112 | case <-h.signal: 113 | return nil 114 | case h.ch <- b: 115 | } 116 | 117 | // 3. push batch to server receive queue: 118 | if err := h.cb.OnEvents(b); err != nil { 119 | return nil 120 | } 121 | } 122 | } 123 | 124 | func (h *defaultHandler) ackLoop() { 125 | log.Println("start client ack loop") 126 | defer log.Println("client ack loop stopped") 127 | 128 | // drain queue on shutdown. 129 | // Stop ACKing batches in case of error, forcing client to reconnect 130 | defer func() { 131 | log.Println("drain ack loop") 132 | //nolint:revive // This drains the channel. 133 | for range h.ch { 134 | } 135 | }() 136 | 137 | for { 138 | select { 139 | case <-h.signal: // return on client/server shutdown 140 | log.Println("receive client connection close signal") 141 | return 142 | case b, open := <-h.ch: 143 | if !open { 144 | return 145 | } 146 | if err := h.waitACK(b); err != nil { 147 | return 148 | } 149 | } 150 | } 151 | } 152 | 153 | func (h *defaultHandler) waitACK(batch *lj.Batch) error { 154 | n := len(batch.Events) 155 | 156 | if h.keepalive <= 0 { 157 | for { 158 | select { 159 | case <-h.signal: 160 | return nil 161 | case <-batch.Await(): 162 | // send ack 163 | return h.writer.ACK(n) 164 | } 165 | } 166 | } else { 167 | for { 168 | select { 169 | case <-h.signal: 170 | return nil 171 | case <-batch.Await(): 172 | // send ack 173 | return h.writer.ACK(n) 174 | case <-time.After(h.keepalive): 175 | if err := h.writer.Keepalive(0); err != nil { 176 | return err 177 | } 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /server/internal/server.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package internal 19 | 20 | import ( 21 | "crypto/tls" 22 | "io" 23 | "net" 24 | "sync" 25 | 26 | "github.com/elastic/go-lumber/lj" 27 | "github.com/elastic/go-lumber/log" 28 | ) 29 | 30 | type Server struct { 31 | listener net.Listener 32 | opts Config 33 | ch chan *lj.Batch 34 | ownCH bool 35 | sig closeSignaler 36 | } 37 | 38 | type Config struct { 39 | TLS *tls.Config 40 | Handler HandlerFactory 41 | Channel chan *lj.Batch 42 | } 43 | 44 | type Handler interface { 45 | Run() 46 | Stop() 47 | } 48 | 49 | type HandlerFactory func(Eventer, net.Conn) (Handler, error) 50 | 51 | type Eventer interface { 52 | OnEvents(*lj.Batch) error 53 | } 54 | 55 | type chanCallback struct { 56 | done <-chan struct{} 57 | ch chan *lj.Batch 58 | } 59 | 60 | func newChanCallback(done <-chan struct{}, ch chan *lj.Batch) *chanCallback { 61 | return &chanCallback{done, ch} 62 | } 63 | 64 | func (c *chanCallback) OnEvents(b *lj.Batch) error { 65 | select { 66 | case <-c.done: 67 | return io.EOF 68 | case c.ch <- b: 69 | return nil 70 | } 71 | } 72 | 73 | func NewWithListener(l net.Listener, opts Config) (*Server, error) { 74 | s := &Server{ 75 | listener: l, 76 | sig: makeCloseSignaler(), 77 | ch: opts.Channel, 78 | opts: opts, 79 | } 80 | 81 | if s.ch == nil { 82 | s.ownCH = true 83 | s.ch = make(chan *lj.Batch, 128) 84 | } 85 | 86 | s.sig.Add(1) 87 | go s.run() 88 | 89 | return s, nil 90 | } 91 | 92 | func ListenAndServeWith( 93 | binder func(network, addr string) (net.Listener, error), 94 | addr string, 95 | opts Config, 96 | ) (*Server, error) { 97 | l, err := binder("tcp", addr) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return NewWithListener(l, opts) 102 | } 103 | 104 | func ListenAndServe(addr string, opts Config) (*Server, error) { 105 | binder := net.Listen 106 | if opts.TLS != nil { 107 | binder = func(network, addr string) (net.Listener, error) { 108 | return tls.Listen(network, addr, opts.TLS) 109 | } 110 | } 111 | 112 | return ListenAndServeWith(binder, addr, opts) 113 | } 114 | 115 | func (s *Server) Close() error { 116 | err := s.listener.Close() 117 | s.sig.Close() 118 | if s.ownCH { 119 | close(s.ch) 120 | } 121 | return err 122 | } 123 | 124 | func (s *Server) Receive() *lj.Batch { 125 | select { 126 | case <-s.sig.Sig(): 127 | return nil 128 | case b := <-s.ch: 129 | return b 130 | } 131 | } 132 | 133 | func (s *Server) ReceiveChan() <-chan *lj.Batch { 134 | return s.ch 135 | } 136 | 137 | func (s *Server) run() { 138 | defer s.sig.Done() 139 | 140 | for { 141 | client, err := s.listener.Accept() 142 | if err != nil { 143 | break 144 | } 145 | 146 | log.Printf("New connection from %v", client.RemoteAddr()) 147 | s.startConnHandler(client) 148 | } 149 | } 150 | 151 | func (s *Server) startConnHandler(client net.Conn) { 152 | var wgStart sync.WaitGroup 153 | 154 | h, err := s.opts.Handler(newChanCallback(s.sig.Sig(), s.ch), client) 155 | if err != nil { 156 | log.Printf("Failed to initialize client handler: %v", h) 157 | return 158 | } 159 | 160 | s.sig.Add(1) 161 | wgStart.Add(1) 162 | stopped := make(chan struct{}, 1) 163 | go func() { 164 | defer s.sig.Done() 165 | defer close(stopped) // signal handler loop stopped 166 | 167 | wgStart.Done() 168 | h.Run() 169 | }() 170 | 171 | wgStart.Wait() 172 | go func() { 173 | select { 174 | case <-s.sig.Sig(): 175 | // server shutdown 176 | h.Stop() 177 | 178 | case <-stopped: 179 | // handler loop stopped 180 | } 181 | }() 182 | } 183 | -------------------------------------------------------------------------------- /server/internal/sig.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package internal 19 | 20 | import "sync" 21 | 22 | type closeSignaler struct { 23 | done chan struct{} 24 | wg sync.WaitGroup 25 | } 26 | 27 | func makeCloseSignaler() closeSignaler { 28 | return closeSignaler{ 29 | done: make(chan struct{}), 30 | } 31 | } 32 | 33 | func (s *closeSignaler) Add(n int) { 34 | s.wg.Add(n) 35 | } 36 | 37 | func (s *closeSignaler) Sig() <-chan struct{} { 38 | return s.done 39 | } 40 | 41 | func (s *closeSignaler) Done() { 42 | s.wg.Done() 43 | } 44 | 45 | func (s *closeSignaler) Close() { 46 | close(s.done) 47 | s.wg.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /server/mux.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package server 19 | 20 | import ( 21 | "errors" 22 | "net" 23 | ) 24 | 25 | type muxListener struct { 26 | net.Listener 27 | ch chan net.Conn 28 | } 29 | 30 | type muxConn struct { 31 | net.Conn 32 | } 33 | 34 | type versionConn struct { 35 | net.Conn 36 | parent *muxConn 37 | v byte 38 | } 39 | 40 | // ErrListenerClosed indicates the multiplexing network listener being closed. 41 | var ErrListenerClosed = errors.New("listener closed") 42 | 43 | func newMuxListener(l net.Listener) *muxListener { 44 | return &muxListener{l, make(chan net.Conn, 1)} 45 | } 46 | 47 | // Accept waits for and returns the next connection to the listener. 48 | func (l *muxListener) Accept() (net.Conn, error) { 49 | conn, ok := <-l.ch 50 | if !ok { 51 | return nil, ErrListenerClosed 52 | } 53 | return conn, nil 54 | } 55 | 56 | // Close closes the listener. 57 | // Any blocked Accept operations will be unblocked and return errors. 58 | func (l *muxListener) Close() error { 59 | close(l.ch) 60 | return nil 61 | } 62 | 63 | func newMuxConn(v byte, c net.Conn) *muxConn { 64 | mc := &muxConn{} 65 | vc := &versionConn{c, mc, v} 66 | mc.Conn = vc 67 | return mc 68 | } 69 | 70 | func (vc *versionConn) Read(buf []byte) (int, error) { 71 | if len(buf) == 0 { 72 | return 0, nil 73 | } 74 | 75 | buf[0] = vc.v 76 | vc.parent.Conn = vc.Conn 77 | n, err := vc.Conn.Read(buf[1:]) 78 | return n + 1, err 79 | } 80 | -------------------------------------------------------------------------------- /server/opts.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package server 19 | 20 | import ( 21 | "crypto/tls" 22 | "encoding/json" 23 | "errors" 24 | "time" 25 | 26 | "github.com/elastic/go-lumber/lj" 27 | ) 28 | 29 | // Option type for configuring server run options. 30 | type Option func(*options) error 31 | 32 | type options struct { 33 | timeout time.Duration 34 | keepalive time.Duration 35 | decoder jsonDecoder 36 | tls *tls.Config 37 | v1 bool 38 | v2 bool 39 | ch chan *lj.Batch 40 | } 41 | 42 | type jsonDecoder func([]byte, interface{}) error 43 | 44 | // Keepalive configures the keepalive interval returning an ACK of length 0 to 45 | // lumberjack client, notifying clients the batch being still active. 46 | func Keepalive(kl time.Duration) Option { 47 | return func(opt *options) error { 48 | if kl < 0 { 49 | return errors.New("keepalive must not be negative") 50 | } 51 | opt.keepalive = kl 52 | return nil 53 | } 54 | } 55 | 56 | // Timeout configures server network timeouts. 57 | func Timeout(to time.Duration) Option { 58 | return func(opt *options) error { 59 | if to < 0 { 60 | return errors.New("timeouts must not be negative") 61 | } 62 | opt.timeout = to 63 | return nil 64 | } 65 | } 66 | 67 | // TLS enables and configures TLS support in lumberjack server. 68 | func TLS(tls *tls.Config) Option { 69 | return func(opt *options) error { 70 | opt.tls = tls 71 | return nil 72 | } 73 | } 74 | 75 | // Channel option is used to register custom channel received batches will be 76 | // forwarded to. 77 | func Channel(c chan *lj.Batch) Option { 78 | return func(opt *options) error { 79 | opt.ch = c 80 | return nil 81 | } 82 | } 83 | 84 | // JSONDecoder sets an alternative json decoder for parsing events if protocol 85 | // version 2 is enabled. The default is json.Unmarshal. 86 | func JSONDecoder(decoder func([]byte, interface{}) error) Option { 87 | return func(opt *options) error { 88 | opt.decoder = decoder 89 | return nil 90 | } 91 | } 92 | 93 | // V1 enables lumberjack protocol version 1. 94 | func V1(b bool) Option { 95 | return func(opt *options) error { 96 | opt.v1 = b 97 | return nil 98 | } 99 | } 100 | 101 | // V2 enables lumberjack protocol version 2. 102 | func V2(b bool) Option { 103 | return func(opt *options) error { 104 | opt.v2 = b 105 | return nil 106 | } 107 | } 108 | 109 | func applyOptions(opts []Option) (options, error) { 110 | o := options{ 111 | decoder: json.Unmarshal, 112 | timeout: 30 * time.Second, 113 | keepalive: 3 * time.Second, 114 | v1: true, 115 | v2: true, 116 | tls: nil, 117 | } 118 | 119 | for _, opt := range opts { 120 | if err := opt(&o); err != nil { 121 | return o, err 122 | } 123 | } 124 | return o, nil 125 | } 126 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package server 19 | 20 | import ( 21 | "crypto/tls" 22 | "errors" 23 | "io" 24 | "net" 25 | "sync" 26 | 27 | "github.com/elastic/go-lumber/lj" 28 | "github.com/elastic/go-lumber/log" 29 | v1 "github.com/elastic/go-lumber/server/v1" 30 | v2 "github.com/elastic/go-lumber/server/v2" 31 | ) 32 | 33 | // Server serves multiple lumberjack clients. 34 | type Server interface { 35 | // ReceiveChan returns a channel all received batch requests will be made 36 | // available on. Batches read from channel must be ACKed. 37 | ReceiveChan() <-chan *lj.Batch 38 | 39 | // Receive returns the next received batch from the receiver channel. 40 | // Batches returned by Receive must be ACKed. 41 | Receive() *lj.Batch 42 | 43 | // Close stops the listener, closes all active connections and closes the 44 | // receiver channel returned from ReceiveChan(). 45 | Close() error 46 | } 47 | 48 | type server struct { 49 | ch chan *lj.Batch 50 | ownCH bool 51 | 52 | done chan struct{} 53 | wg sync.WaitGroup 54 | 55 | netListener net.Listener 56 | mux []muxServer 57 | } 58 | 59 | type muxServer struct { 60 | mux byte 61 | l *muxListener 62 | server Server 63 | } 64 | 65 | // ErrNoVersionEnabled indicates no lumberjack protocol version being enabled 66 | // when instantiating a server. 67 | var ErrNoVersionEnabled = errors.New("no protocol version enabled") 68 | 69 | // NewWithListener creates a new Server using an existing net.Listener. Use 70 | // options V1 and V2 to enable wanted protocol versions. 71 | func NewWithListener(l net.Listener, opts ...Option) (Server, error) { 72 | return newServer(l, opts...) 73 | } 74 | 75 | // ListenAndServeWith uses binder to create a listener for establishing a lumberjack 76 | // endpoint. 77 | // Use options V1 and V2 to enable wanted protocol versions. 78 | func ListenAndServeWith( 79 | binder func(network, addr string) (net.Listener, error), 80 | addr string, 81 | opts ...Option, 82 | ) (Server, error) { 83 | l, err := binder("tcp", addr) 84 | if err != nil { 85 | return nil, err 86 | } 87 | s, err := NewWithListener(l, opts...) 88 | if err != nil { 89 | l.Close() 90 | } 91 | return s, err 92 | } 93 | 94 | // ListenAndServe listens on the TCP network address addr and handles batch 95 | // requests from accepted lumberjack clients. 96 | // Use options V1 and V2 to enable wanted protocol versions. 97 | func ListenAndServe(addr string, opts ...Option) (Server, error) { 98 | o, err := applyOptions(opts) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | binder := net.Listen 104 | if o.tls != nil { 105 | binder = func(network, addr string) (net.Listener, error) { 106 | return tls.Listen(network, addr, o.tls) 107 | } 108 | } 109 | 110 | return ListenAndServeWith(binder, addr, opts...) 111 | } 112 | 113 | // Close stops the listener, closes all active connections and closes the 114 | // receiver channel returned from ReceiveChan() 115 | func (s *server) Close() error { 116 | close(s.done) 117 | for _, m := range s.mux { 118 | m.server.Close() 119 | } 120 | err := s.netListener.Close() 121 | s.wg.Wait() 122 | if s.ownCH { 123 | close(s.ch) 124 | } 125 | return err 126 | } 127 | 128 | // ReceiveChan returns a channel all received batch requests will be made 129 | // available on. Batches read from channel must be ACKed. 130 | func (s *server) ReceiveChan() <-chan *lj.Batch { 131 | return s.ch 132 | } 133 | 134 | // Receive returns the next received batch from the receiver channel. 135 | // Batches returned by Receive must be ACKed. 136 | func (s *server) Receive() *lj.Batch { 137 | select { 138 | case <-s.done: 139 | return nil 140 | case b := <-s.ch: 141 | return b 142 | } 143 | } 144 | 145 | func newServer(l net.Listener, opts ...Option) (Server, error) { 146 | cfg, err := applyOptions(opts) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | var servers []func(net.Listener) (Server, byte, error) 152 | 153 | log.Printf("Server config: %#v", cfg) 154 | 155 | if cfg.v1 { 156 | servers = append(servers, func(l net.Listener) (Server, byte, error) { 157 | s, err := v1.NewWithListener(l, 158 | v1.Timeout(cfg.timeout), 159 | v1.Channel(cfg.ch), 160 | v1.TLS(cfg.tls)) 161 | return s, '1', err 162 | }) 163 | } 164 | if cfg.v2 { 165 | servers = append(servers, func(l net.Listener) (Server, byte, error) { 166 | s, err := v2.NewWithListener(l, 167 | v2.Keepalive(cfg.keepalive), 168 | v2.Timeout(cfg.timeout), 169 | v2.Channel(cfg.ch), 170 | v2.TLS(cfg.tls), 171 | v2.JSONDecoder(cfg.decoder)) 172 | return s, '2', err 173 | }) 174 | } 175 | 176 | if len(servers) == 0 { 177 | return nil, ErrNoVersionEnabled 178 | } 179 | if len(servers) == 1 { 180 | s, _, err := servers[0](l) 181 | return s, err 182 | } 183 | 184 | ownCH := false 185 | if cfg.ch == nil { 186 | ownCH = true 187 | cfg.ch = make(chan *lj.Batch, 128) 188 | } 189 | 190 | mux := make([]muxServer, len(servers)) 191 | for i, mk := range servers { 192 | muxL := newMuxListener(l) 193 | log.Printf("mk: %v", i) 194 | s, b, err := mk(muxL) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | mux[i] = muxServer{ 200 | mux: b, 201 | l: muxL, 202 | server: s, 203 | } 204 | } 205 | 206 | s := &server{ 207 | ch: cfg.ch, 208 | ownCH: ownCH, 209 | netListener: l, 210 | mux: mux, 211 | done: make(chan struct{}), 212 | } 213 | s.wg.Add(1) 214 | go s.run() 215 | 216 | return s, nil 217 | } 218 | 219 | func (s *server) run() { 220 | defer s.wg.Done() 221 | for { 222 | client, err := s.netListener.Accept() 223 | if err != nil { 224 | break 225 | } 226 | 227 | s.handle(client) 228 | } 229 | } 230 | 231 | func (s *server) handle(client net.Conn) { 232 | // read first byte and decide multiplexer 233 | 234 | sig := make(chan struct{}) 235 | 236 | go func() { 237 | defer close(sig) 238 | 239 | var buf [1]byte 240 | if _, err := io.ReadFull(client, buf[:]); err != nil { 241 | client.Close() 242 | return 243 | } 244 | 245 | for _, m := range s.mux { 246 | if m.mux != buf[0] { 247 | continue 248 | } 249 | 250 | conn := newMuxConn(buf[0], client) 251 | m.l.ch <- conn 252 | return 253 | } 254 | client.Close() 255 | }() 256 | 257 | go func() { 258 | select { 259 | case <-sig: 260 | case <-s.done: 261 | // close connection if server being shut down 262 | client.Close() 263 | } 264 | }() 265 | } 266 | -------------------------------------------------------------------------------- /server/v1/doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package v1 implements the lumberjack server supporting protocol version 1. 19 | package v1 20 | -------------------------------------------------------------------------------- /server/v1/opts.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v1 19 | 20 | import ( 21 | "crypto/tls" 22 | "errors" 23 | "time" 24 | 25 | "github.com/elastic/go-lumber/lj" 26 | ) 27 | 28 | // Option type for configuring server run options. 29 | type Option func(*options) error 30 | 31 | type options struct { 32 | timeout time.Duration 33 | tls *tls.Config 34 | ch chan *lj.Batch 35 | } 36 | 37 | // Timeout configures server network timeouts. 38 | func Timeout(to time.Duration) Option { 39 | return func(opt *options) error { 40 | if to < 0 { 41 | return errors.New("timeouts must not be negative") 42 | } 43 | opt.timeout = to 44 | return nil 45 | } 46 | } 47 | 48 | // TLS enables and configures TLS support in lumberjack server. 49 | // Protocol version 1 mandates TLS being enabled. 50 | func TLS(tls *tls.Config) Option { 51 | return func(opt *options) error { 52 | opt.tls = tls 53 | return nil 54 | } 55 | } 56 | 57 | // Channel option is used to register custom channel received batches will be 58 | // forwarded to. 59 | func Channel(c chan *lj.Batch) Option { 60 | return func(opt *options) error { 61 | opt.ch = c 62 | return nil 63 | } 64 | } 65 | 66 | func applyOptions(opts []Option) (options, error) { 67 | o := options{ 68 | timeout: 30 * time.Second, 69 | tls: nil, 70 | } 71 | 72 | for _, opt := range opts { 73 | if err := opt(&o); err != nil { 74 | return o, err 75 | } 76 | } 77 | return o, nil 78 | } 79 | -------------------------------------------------------------------------------- /server/v1/reader.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v1 19 | 20 | import ( 21 | "bufio" 22 | "crypto/tls" 23 | "encoding/binary" 24 | "io" 25 | "net" 26 | "time" 27 | 28 | "github.com/klauspost/compress/zlib" 29 | 30 | "github.com/elastic/go-lumber/lj" 31 | "github.com/elastic/go-lumber/log" 32 | protocol "github.com/elastic/go-lumber/protocol/v1" 33 | ) 34 | 35 | type reader struct { 36 | conn net.Conn 37 | in *bufio.Reader 38 | tlsState *tls.ConnectionState 39 | remoteAddr string 40 | buf []byte 41 | timeout time.Duration 42 | } 43 | 44 | func newReader(c net.Conn, to time.Duration) *reader { 45 | r := &reader{ 46 | conn: c, 47 | in: bufio.NewReader(c), 48 | remoteAddr: c.RemoteAddr().String(), 49 | buf: make([]byte, 0, 64), 50 | timeout: to, 51 | } 52 | 53 | if tlsConn, ok := c.(*tls.Conn); ok { 54 | s := tlsConn.ConnectionState() 55 | r.tlsState = &s 56 | } 57 | return r 58 | } 59 | 60 | func (r *reader) ReadBatch() (*lj.Batch, error) { 61 | // 1. read window size 62 | var win [6]byte 63 | _ = r.conn.SetReadDeadline(time.Time{}) // wait for next batch without timeout 64 | if err := readFull(r.in, win[:]); err != nil { 65 | return nil, err 66 | } 67 | 68 | if win[0] != protocol.CodeVersion && win[1] != protocol.CodeWindowSize { 69 | log.Printf("Expected window from. Received %v", win[0:1]) 70 | return nil, ErrProtocolError 71 | } 72 | 73 | count := int(binary.BigEndian.Uint32(win[2:])) 74 | if count == 0 { 75 | return nil, nil 76 | } 77 | 78 | if err := r.conn.SetReadDeadline(time.Now().Add(r.timeout)); err != nil { 79 | return nil, err 80 | } 81 | 82 | events, err := r.readEvents(r.in, make([]interface{}, 0, count)) 83 | if events == nil || err != nil { 84 | log.Printf("readEvents failed with: %v", err) 85 | return nil, err 86 | } 87 | 88 | return lj.NewBatchWithSourceMetadata(events, r.remoteAddr, r.tlsState), nil 89 | } 90 | 91 | func (r *reader) readEvents(in io.Reader, events []interface{}) ([]interface{}, error) { 92 | for len(events) < cap(events) { 93 | var hdr [2]byte 94 | if err := readFull(in, hdr[:]); err != nil { 95 | return nil, err 96 | } 97 | 98 | if hdr[0] != protocol.CodeVersion { 99 | log.Println("Event protocol version error") 100 | return nil, ErrProtocolError 101 | } 102 | 103 | switch hdr[1] { 104 | case protocol.CodeDataFrame: 105 | event, err := r.readEvent(in) 106 | if err != nil { 107 | log.Printf("failed to read json event with: %v\n", err) 108 | return nil, err 109 | } 110 | events = append(events, event) 111 | case protocol.CodeCompressed: 112 | readEvents, err := r.readCompressed(in, events) 113 | if err != nil { 114 | return nil, err 115 | } 116 | events = readEvents 117 | default: 118 | log.Printf("Unknown frame type: %v", hdr[1]) 119 | return nil, ErrProtocolError 120 | } 121 | } 122 | return events, nil 123 | } 124 | 125 | func (r *reader) readCompressed(in io.Reader, events []interface{}) ([]interface{}, error) { 126 | var hdr [4]byte 127 | if err := readFull(in, hdr[:]); err != nil { 128 | return nil, err 129 | } 130 | 131 | payloadSz := binary.BigEndian.Uint32(hdr[:]) 132 | limit := io.LimitReader(in, int64(payloadSz)) 133 | reader, err := zlib.NewReader(limit) 134 | if err != nil { 135 | log.Printf("Failed to initialized zlib reader %v\n", err) 136 | return nil, err 137 | } 138 | 139 | events, err = r.readEvents(reader, events) 140 | if err != nil { 141 | _ = reader.Close() 142 | return nil, err 143 | } 144 | if err := reader.Close(); err != nil { 145 | return nil, err 146 | } 147 | 148 | // consume final bytes from limit reader 149 | for { 150 | var tmp [16]byte 151 | if _, err := limit.Read(tmp[:]); err != nil { 152 | if err != io.EOF { 153 | return nil, err 154 | } 155 | break 156 | } 157 | } 158 | return events, nil 159 | } 160 | 161 | func (r *reader) readEvent(in io.Reader) (interface{}, error) { 162 | var hdr [8]byte 163 | if err := readFull(in, hdr[:]); err != nil { 164 | return nil, err 165 | } 166 | 167 | readString := func() (string, error) { 168 | var bufBytes [4]byte 169 | if err := readFull(in, bufBytes[:]); err != nil { 170 | return "", err 171 | } 172 | 173 | bytes := int(binary.BigEndian.Uint32(bufBytes[:])) 174 | if bytes > len(r.buf) { 175 | r.buf = make([]byte, bytes) 176 | } 177 | 178 | buf := r.buf[:bytes] 179 | if err := readFull(in, buf); err != nil { 180 | return "", err 181 | } 182 | 183 | return string(buf[:]), nil 184 | } 185 | 186 | event := map[string]string{} 187 | pairs := int(binary.BigEndian.Uint32(hdr[4:])) 188 | for i := 0; i < pairs; i++ { 189 | k, err := readString() 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | v, err := readString() 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | event[k] = v 200 | } 201 | return event, nil 202 | } 203 | 204 | func readFull(in io.Reader, buf []byte) error { 205 | _, err := io.ReadFull(in, buf) 206 | return err 207 | } 208 | -------------------------------------------------------------------------------- /server/v1/server.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v1 19 | 20 | import ( 21 | "errors" 22 | "net" 23 | 24 | "github.com/elastic/go-lumber/lj" 25 | "github.com/elastic/go-lumber/server/internal" 26 | ) 27 | 28 | // Server serves multiple lumberjack clients supporting protocol version 1. 29 | type Server struct { 30 | s *internal.Server 31 | } 32 | 33 | // ErrProtocolError is returned if a protocol error was detected in the 34 | // conversation with lumberjack server. 35 | var ErrProtocolError = errors.New("lumberjack protocol error") 36 | 37 | // NewWithListener creates a new Server using an existing net.Listener. 38 | func NewWithListener(l net.Listener, opts ...Option) (*Server, error) { 39 | return newServer(opts, func(cfg internal.Config) (*internal.Server, error) { 40 | return internal.NewWithListener(l, cfg) 41 | }) 42 | } 43 | 44 | // ListenAndServeWith uses binder to create a listener for establishing a lumberjack 45 | // endpoint. 46 | func ListenAndServeWith( 47 | binder func(network, addr string) (net.Listener, error), 48 | addr string, 49 | opts ...Option, 50 | ) (*Server, error) { 51 | return newServer(opts, func(cfg internal.Config) (*internal.Server, error) { 52 | return internal.ListenAndServeWith(binder, addr, cfg) 53 | }) 54 | } 55 | 56 | // ListenAndServe listens on the TCP network address addr and handles batch 57 | // requests from accepted lumberjack clients. 58 | func ListenAndServe(addr string, opts ...Option) (*Server, error) { 59 | return newServer(opts, func(cfg internal.Config) (*internal.Server, error) { 60 | return internal.ListenAndServe(addr, cfg) 61 | }) 62 | } 63 | 64 | // ReceiveChan returns a channel all received batch requests will be made 65 | // available on. Batches read from channel must be ACKed. 66 | func (s *Server) ReceiveChan() <-chan *lj.Batch { 67 | return s.s.ReceiveChan() 68 | } 69 | 70 | // Receive returns the next received batch from the receiver channel. 71 | // Batches returned by Receive must be ACKed. 72 | func (s *Server) Receive() *lj.Batch { 73 | return s.s.Receive() 74 | } 75 | 76 | // Close stops the listener, closes all active connections and closes the 77 | // receiver channel returned from ReceiveChan(). 78 | func (s *Server) Close() error { 79 | return s.s.Close() 80 | } 81 | 82 | func newServer( 83 | opts []Option, 84 | mk func(cfg internal.Config) (*internal.Server, error), 85 | ) (*Server, error) { 86 | o, err := applyOptions(opts) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | mkRW := func(client net.Conn) (internal.BatchReader, internal.ACKWriter, error) { 92 | r := newReader(client, o.timeout) 93 | w := newWriter(client, o.timeout) 94 | return r, w, nil 95 | } 96 | 97 | cfg := internal.Config{ 98 | TLS: o.tls, 99 | Handler: internal.DefaultHandler(0, mkRW), 100 | Channel: o.ch, 101 | } 102 | 103 | s, err := mk(cfg) 104 | return &Server{s}, err 105 | } 106 | -------------------------------------------------------------------------------- /server/v1/writer.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v1 19 | 20 | import ( 21 | "encoding/binary" 22 | "net" 23 | "time" 24 | 25 | protocol "github.com/elastic/go-lumber/protocol/v1" 26 | ) 27 | 28 | type writer struct { 29 | c net.Conn 30 | to time.Duration 31 | } 32 | 33 | func newWriter(c net.Conn, to time.Duration) *writer { 34 | return &writer{c: c, to: to} 35 | } 36 | 37 | func (w *writer) ACK(n int) error { 38 | var buf [6]byte 39 | buf[0] = protocol.CodeVersion 40 | buf[1] = protocol.CodeACK 41 | binary.BigEndian.PutUint32(buf[2:], uint32(n)) 42 | 43 | if err := w.c.SetWriteDeadline(time.Now().Add(w.to)); err != nil { 44 | return err 45 | } 46 | 47 | tmp := buf[:] 48 | for len(tmp) > 0 { 49 | n, err := w.c.Write(tmp) 50 | if err != nil { 51 | return err 52 | } 53 | tmp = tmp[n:] 54 | } 55 | return nil 56 | } 57 | 58 | func (*writer) Keepalive(int) error { 59 | // keepalive not supported by v1 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /server/v2/doc.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | // Package v2 implements the lumberjack server supporting protocol version 2. 19 | package v2 20 | -------------------------------------------------------------------------------- /server/v2/opts.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "crypto/tls" 22 | "encoding/json" 23 | "errors" 24 | "time" 25 | 26 | "github.com/elastic/go-lumber/lj" 27 | ) 28 | 29 | // Option type for configuring server run options. 30 | type Option func(*options) error 31 | 32 | type options struct { 33 | timeout time.Duration 34 | keepalive time.Duration 35 | decoder jsonDecoder 36 | tls *tls.Config 37 | ch chan *lj.Batch 38 | } 39 | 40 | // Keepalive configures the keepalive interval returning an ACK of length 0 to 41 | // lumberjack client, notifying clients the batch being still active. 42 | func Keepalive(kl time.Duration) Option { 43 | return func(opt *options) error { 44 | if kl < 0 { 45 | return errors.New("keepalive must not be negative") 46 | } 47 | opt.keepalive = kl 48 | return nil 49 | } 50 | } 51 | 52 | // Timeout configures server network timeouts. 53 | func Timeout(to time.Duration) Option { 54 | return func(opt *options) error { 55 | if to < 0 { 56 | return errors.New("timeouts must not be negative") 57 | } 58 | opt.timeout = to 59 | return nil 60 | } 61 | } 62 | 63 | // Channel option is used to register custom channel received batches will be 64 | // forwarded to. 65 | func Channel(c chan *lj.Batch) Option { 66 | return func(opt *options) error { 67 | opt.ch = c 68 | return nil 69 | } 70 | } 71 | 72 | // TLS enables and configures TLS support in lumberjack server. 73 | func TLS(tls *tls.Config) Option { 74 | return func(opt *options) error { 75 | opt.tls = tls 76 | return nil 77 | } 78 | } 79 | 80 | // JSONDecoder sets an alternative json decoder for parsing events. 81 | // The default is json.Unmarshal. 82 | func JSONDecoder(decoder func([]byte, interface{}) error) Option { 83 | return func(opt *options) error { 84 | opt.decoder = decoder 85 | return nil 86 | } 87 | } 88 | 89 | func applyOptions(opts []Option) (options, error) { 90 | o := options{ 91 | decoder: json.Unmarshal, 92 | timeout: 30 * time.Second, 93 | keepalive: 3 * time.Second, 94 | tls: nil, 95 | } 96 | 97 | for _, opt := range opts { 98 | if err := opt(&o); err != nil { 99 | return o, err 100 | } 101 | } 102 | return o, nil 103 | } 104 | -------------------------------------------------------------------------------- /server/v2/reader.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "bufio" 22 | "crypto/tls" 23 | "encoding/binary" 24 | "io" 25 | "net" 26 | "time" 27 | 28 | "github.com/klauspost/compress/zlib" 29 | 30 | "github.com/elastic/go-lumber/lj" 31 | "github.com/elastic/go-lumber/log" 32 | protocol "github.com/elastic/go-lumber/protocol/v2" 33 | ) 34 | 35 | type reader struct { 36 | conn net.Conn 37 | in *bufio.Reader 38 | tlsState *tls.ConnectionState 39 | decoder jsonDecoder 40 | remoteAddr string 41 | buf []byte 42 | timeout time.Duration 43 | } 44 | 45 | type jsonDecoder func([]byte, interface{}) error 46 | 47 | func newReader(c net.Conn, to time.Duration, jsonDecoder jsonDecoder) *reader { 48 | r := &reader{ 49 | conn: c, 50 | in: bufio.NewReader(c), 51 | decoder: jsonDecoder, 52 | remoteAddr: c.RemoteAddr().String(), 53 | buf: make([]byte, 0, 64), 54 | timeout: to, 55 | } 56 | 57 | if tlsConn, ok := c.(*tls.Conn); ok { 58 | s := tlsConn.ConnectionState() 59 | r.tlsState = &s 60 | } 61 | return r 62 | } 63 | 64 | func (r *reader) ReadBatch() (*lj.Batch, error) { 65 | // 1. read window size 66 | var win [6]byte 67 | _ = r.conn.SetReadDeadline(time.Time{}) // wait for next batch without timeout 68 | if err := readFull(r.in, win[:]); err != nil { 69 | return nil, err 70 | } 71 | 72 | if win[0] != protocol.CodeVersion && win[1] != protocol.CodeWindowSize { 73 | log.Printf("Expected window from. Received %v", win[0:1]) 74 | return nil, ErrProtocolError 75 | } 76 | 77 | count := int(binary.BigEndian.Uint32(win[2:])) 78 | if count == 0 { 79 | return nil, nil 80 | } 81 | 82 | if err := r.conn.SetReadDeadline(time.Now().Add(r.timeout)); err != nil { 83 | return nil, err 84 | } 85 | 86 | events, err := r.readEvents(r.in, make([]interface{}, 0, count)) 87 | if events == nil || err != nil { 88 | log.Printf("readEvents failed with: %v", err) 89 | return nil, err 90 | } 91 | 92 | return lj.NewBatchWithSourceMetadata(events, r.remoteAddr, r.tlsState), nil 93 | } 94 | 95 | func (r *reader) readEvents(in io.Reader, events []interface{}) ([]interface{}, error) { 96 | for len(events) < cap(events) { 97 | var hdr [2]byte 98 | if err := readFull(in, hdr[:]); err != nil { 99 | return nil, err 100 | } 101 | 102 | if hdr[0] != protocol.CodeVersion { 103 | log.Println("Event protocol version error") 104 | return nil, ErrProtocolError 105 | } 106 | 107 | switch hdr[1] { 108 | case protocol.CodeJSONDataFrame: 109 | event, err := r.readJSONEvent(in) 110 | if err != nil { 111 | log.Printf("failed to read json event with: %v\n", err) 112 | return nil, err 113 | } 114 | events = append(events, event) 115 | case protocol.CodeCompressed: 116 | readEvents, err := r.readCompressed(in, events) 117 | if err != nil { 118 | return nil, err 119 | } 120 | events = readEvents 121 | default: 122 | log.Printf("Unknown frame type: %v", hdr[1]) 123 | return nil, ErrProtocolError 124 | } 125 | } 126 | return events, nil 127 | } 128 | 129 | func (r *reader) readJSONEvent(in io.Reader) (interface{}, error) { 130 | var hdr [8]byte 131 | if err := readFull(in, hdr[:]); err != nil { 132 | return nil, err 133 | } 134 | 135 | payloadSz := int(binary.BigEndian.Uint32(hdr[4:])) 136 | if payloadSz > len(r.buf) { 137 | r.buf = make([]byte, payloadSz) 138 | } 139 | 140 | buf := r.buf[:payloadSz] 141 | if err := readFull(in, buf); err != nil { 142 | return nil, err 143 | } 144 | 145 | var event interface{} 146 | err := r.decoder(buf, &event) 147 | return event, err 148 | } 149 | 150 | func (r *reader) readCompressed(in io.Reader, events []interface{}) ([]interface{}, error) { 151 | var hdr [4]byte 152 | if err := readFull(in, hdr[:]); err != nil { 153 | return nil, err 154 | } 155 | 156 | payloadSz := binary.BigEndian.Uint32(hdr[:]) 157 | limit := io.LimitReader(in, int64(payloadSz)) 158 | reader, err := zlib.NewReader(limit) 159 | if err != nil { 160 | log.Printf("Failed to initialized zlib reader %v\n", err) 161 | return nil, err 162 | } 163 | 164 | events, err = r.readEvents(reader, events) 165 | if err != nil { 166 | _ = reader.Close() 167 | return nil, err 168 | } 169 | if err := reader.Close(); err != nil { 170 | return nil, err 171 | } 172 | 173 | // consume final bytes from limit reader 174 | for { 175 | var tmp [16]byte 176 | if _, err := limit.Read(tmp[:]); err != nil { 177 | if err != io.EOF { 178 | return nil, err 179 | } 180 | break 181 | } 182 | } 183 | return events, nil 184 | } 185 | 186 | func readFull(in io.Reader, buf []byte) error { 187 | _, err := io.ReadFull(in, buf) 188 | return err 189 | } 190 | -------------------------------------------------------------------------------- /server/v2/server.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "errors" 22 | "net" 23 | 24 | "github.com/elastic/go-lumber/lj" 25 | "github.com/elastic/go-lumber/server/internal" 26 | ) 27 | 28 | // Server serves multiple lumberjack clients supporting protocol version 2. 29 | type Server struct { 30 | s *internal.Server 31 | } 32 | 33 | // ErrProtocolError is returned if a protocol error was detected in the 34 | // conversation with lumberjack server. 35 | var ErrProtocolError = errors.New("lumberjack protocol error") 36 | 37 | // NewWithListener creates a new Server using an existing net.Listener. 38 | func NewWithListener(l net.Listener, opts ...Option) (*Server, error) { 39 | return newServer(opts, func(cfg internal.Config) (*internal.Server, error) { 40 | return internal.NewWithListener(l, cfg) 41 | }) 42 | } 43 | 44 | // ListenAndServeWith uses binder to create a listener for establishing a lumberjack 45 | // endpoint. 46 | func ListenAndServeWith( 47 | binder func(network, addr string) (net.Listener, error), 48 | addr string, 49 | opts ...Option, 50 | ) (*Server, error) { 51 | return newServer(opts, func(cfg internal.Config) (*internal.Server, error) { 52 | return internal.ListenAndServeWith(binder, addr, cfg) 53 | }) 54 | } 55 | 56 | // ListenAndServe listens on the TCP network address addr and handles batch 57 | // requests from accepted lumberjack clients. 58 | func ListenAndServe(addr string, opts ...Option) (*Server, error) { 59 | return newServer(opts, func(cfg internal.Config) (*internal.Server, error) { 60 | return internal.ListenAndServe(addr, cfg) 61 | }) 62 | } 63 | 64 | // ReceiveChan returns a channel all received batch requests will be made 65 | // available on. Batches read from channel must be ACKed. 66 | func (s *Server) ReceiveChan() <-chan *lj.Batch { 67 | return s.s.ReceiveChan() 68 | } 69 | 70 | // Receive returns the next received batch from the receiver channel. 71 | // Batches returned by Receive must be ACKed. 72 | func (s *Server) Receive() *lj.Batch { 73 | return s.s.Receive() 74 | } 75 | 76 | // Close stops the listener, closes all active connections and closes the 77 | // receiver channel returned from ReceiveChan(). 78 | func (s *Server) Close() error { 79 | return s.s.Close() 80 | } 81 | 82 | func newServer( 83 | opts []Option, 84 | mk func(cfg internal.Config) (*internal.Server, error), 85 | ) (*Server, error) { 86 | o, err := applyOptions(opts) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | mkRW := func(client net.Conn) (internal.BatchReader, internal.ACKWriter, error) { 92 | r := newReader(client, o.timeout, o.decoder) 93 | w := newWriter(client, o.timeout) 94 | return r, w, nil 95 | } 96 | 97 | cfg := internal.Config{ 98 | TLS: o.tls, 99 | Handler: internal.DefaultHandler(o.keepalive, mkRW), 100 | Channel: o.ch, 101 | } 102 | 103 | s, err := mk(cfg) 104 | return &Server{s}, err 105 | } 106 | -------------------------------------------------------------------------------- /server/v2/writer.go: -------------------------------------------------------------------------------- 1 | // Licensed to Elasticsearch B.V. under one or more contributor 2 | // license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright 4 | // ownership. Elasticsearch B.V. licenses this file to you under 5 | // the Apache License, Version 2.0 (the "License"); you may 6 | // not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | package v2 19 | 20 | import ( 21 | "encoding/binary" 22 | "net" 23 | "time" 24 | 25 | protocol "github.com/elastic/go-lumber/protocol/v2" 26 | ) 27 | 28 | type writer struct { 29 | c net.Conn 30 | to time.Duration 31 | } 32 | 33 | func newWriter(c net.Conn, to time.Duration) *writer { 34 | return &writer{c: c, to: to} 35 | } 36 | 37 | func (w *writer) ACK(n int) error { 38 | var buf [6]byte 39 | buf[0] = protocol.CodeVersion 40 | buf[1] = protocol.CodeACK 41 | binary.BigEndian.PutUint32(buf[2:], uint32(n)) 42 | 43 | if err := w.c.SetWriteDeadline(time.Now().Add(w.to)); err != nil { 44 | return err 45 | } 46 | 47 | tmp := buf[:] 48 | for len(tmp) > 0 { 49 | n, err := w.c.Write(tmp) 50 | if err != nil { 51 | return err 52 | } 53 | tmp = tmp[n:] 54 | } 55 | return nil 56 | } 57 | 58 | func (w *writer) Keepalive(n int) error { 59 | return w.ACK(n) 60 | } 61 | --------------------------------------------------------------------------------