├── .gitignore ├── framer_test.go ├── net_conn.go ├── framer.go ├── .travis.yml ├── test ├── cert.pem └── privkey.pem ├── script ├── coverage └── gen-certs.py ├── constants.go ├── LICENSE ├── srslog_unix.go ├── formatter_test.go ├── formatter.go ├── CODE_OF_CONDUCT.md ├── dialer.go ├── README.md ├── srslog.go ├── writer.go ├── dialer_test.go ├── writer_test.go └── srslog_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .cover 2 | -------------------------------------------------------------------------------- /framer_test.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDefaultFramer(t *testing.T) { 8 | out := DefaultFramer("input message") 9 | if out != "input message" { 10 | t.Errorf("should match the input message") 11 | } 12 | } 13 | 14 | func TestRFC5425MessageLengthFramer(t *testing.T) { 15 | out := RFC5425MessageLengthFramer("input message") 16 | if out != "13 input message" { 17 | t.Errorf("should prepend the input message length") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /net_conn.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // netConn has an internal net.Conn and adheres to the serverConn interface, 8 | // allowing us to send syslog messages over the network. 9 | type netConn struct { 10 | conn net.Conn 11 | } 12 | 13 | // writeString formats syslog messages using time.RFC3339 and includes the 14 | // hostname, and sends the message to the connection. 15 | func (n *netConn) writeString(framer Framer, formatter Formatter, p Priority, hostname, tag, msg string) error { 16 | if framer == nil { 17 | framer = DefaultFramer 18 | } 19 | if formatter == nil { 20 | formatter = DefaultFormatter 21 | } 22 | formattedMessage := framer(formatter(p, hostname, tag, msg)) 23 | _, err := n.conn.Write([]byte(formattedMessage)) 24 | return err 25 | } 26 | 27 | // close the network connection 28 | func (n *netConn) close() error { 29 | return n.conn.Close() 30 | } 31 | -------------------------------------------------------------------------------- /framer.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Framer is a type of function that takes an input string (typically an 8 | // already-formatted syslog message) and applies "message framing" to it. We 9 | // have different framers because different versions of the syslog protocol 10 | // and its transport requirements define different framing behavior. 11 | type Framer func(in string) string 12 | 13 | // DefaultFramer does nothing, since there is no framing to apply. This is 14 | // the original behavior of the Go syslog package, and is also typically used 15 | // for UDP syslog. 16 | func DefaultFramer(in string) string { 17 | return in 18 | } 19 | 20 | // RFC5425MessageLengthFramer prepends the message length to the front of the 21 | // provided message, as defined in RFC 5425. 22 | func RFC5425MessageLengthFramer(in string) string { 23 | return fmt.Sprintf("%d %s", len(in), in) 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | group: edge 4 | language: go 5 | go: 6 | - 1.5 7 | before_install: 8 | - pip install --user codecov 9 | script: 10 | - | 11 | go get ./... 12 | go test -v -coverprofile=coverage.txt -covermode=atomic 13 | go vet 14 | after_success: 15 | - codecov 16 | notifications: 17 | slack: 18 | secure: dtDue9gP6CRR1jYjEf6raXXFak3QKGcCFvCf5mfvv5XScdpmc3udwgqc5TdyjC0goaC9OK/4jTcCD30dYZm/u6ux3E9mo3xwMl2xRLHx76p5r9rSQtloH19BDwA2+A+bpDfFQVz05k2YXuTiGSvNMMdwzx+Dr294Sl/z43RFB4+b9/R/6LlFpRW89IwftvpLAFnBy4K/ZcspQzKM+rQfQTL5Kk+iZ/KBsuR/VziDq6MoJ8t43i4ee8vwS06vFBKDbUiZ4FIZpLgc2RAL5qso5aWRKYXL6waXfoKHZWKPe0w4+9IY1rDJxG1jEb7YGgcbLaF9xzPRRs2b2yO/c87FKpkh6PDgYHfLjpgXotCoojZrL4p1x6MI1ldJr3NhARGPxS9r4liB9n6Y5nD+ErXi1IMf55fuUHcPY27Jc0ySeLFeM6cIWJ8OhFejCgGw6a5DnnmJo0PqopsaBDHhadpLejT1+K6bL2iGkT4SLcVNuRGLs+VyuNf1+5XpkWZvy32vquO7SZOngLLBv+GIem+t3fWm0Z9s/0i1uRCQei1iUutlYjoV/LBd35H2rhob4B5phIuJin9kb0zbHf6HnaoN0CtN8r0d8G5CZiInVlG5Xcid5Byb4dddf5U2EJTDuCMVyyiM7tcnfjqw9UbVYNxtYM9SzcqIq+uVqM8pYL9xSec= 19 | -------------------------------------------------------------------------------- /test/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC4DCCAcigAwIBAgIQK5t5Skr1T6GQQn6u9VSXZTANBgkqhkiG9w0BAQsFADAU 3 | MRIwEAYDVQQDDAkxMjcuMC4wLjEwHhcNMTUxMjA3MTAxMTI0WhcNMTgxMjMxMDAw 4 | MDAwWjAUMRIwEAYDVQQDDAkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IB 5 | DwAwggEKAoIBAQDr9UU6xYOCGQgmXOA3HH6ZW/xKATDzxn58nQWXy/1kqI84NFPb 6 | 9elpWUxU0D86xcU7YZDV1I79N00SaDTYyHAMLHi6WUt55nokLTRA15mxskRbYE8H 7 | 5ANKB1+fBDP1LLtGgIhfU2mkfxJpxtSkcSgmXIi0hstIGzGT98QIsSdUeCpZWF9s 8 | KmajGV7wJtzpNcUO8BOh5sd5s37K2A5C7w9HZvINPV+/1FDP5GalOhg94eprfV0y 9 | 78z/+Sxqr7wc2rTw27cjf5waTgQYgEbzBowMQJP1ojI646vYBrg3TEX7tjnC7f8E 10 | +Y7xe614c0Bj8XhJZbNV0MIG8Ka49HlUTcejAgMBAAGjLjAsMA8GA1UdEQQIMAaH 11 | BH8AAAEwCwYDVR0PBAQDAgGuMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQAD 12 | ggEBAOdc2KSjnQfd8ZS2nAO9opo9D82q+03LrC6ZGun5/NvAazUS+uJ5wyTpx9A5 13 | 45s3Cib7eCrlHsB7UbslUm33+VVeSmj+VCBYkW5iY6kLth007pVonmxCi/hF37dm 14 | y8XoSifJA7CdPRptI8LAHn/H34Awl43vaGHQDLaaCgFBDTtKWxjvdKS2aSvXWvjC 15 | 81k+i5mb1OiJSoioLSgtX94txde/gDI87yrKWNLBQ7MqPzSg8DtRXftUeaLgcKlQ 16 | KjYg0wx90HUwpi5Hv2E1Q09LmecyHuquYpve364xlK1lQjEdkaGn2nNCPUHOiHT7 17 | 1BAIPcvnMumWv7uBYAW5nYCaeRw= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /script/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generate test coverage statistics for Go packages. 3 | # 4 | # Works around the fact that `go test -coverprofile` currently does not work 5 | # with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909 6 | # 7 | # Usage: script/coverage [--html|--coveralls] 8 | # 9 | # --html Additionally create HTML report and open it in browser 10 | # --coveralls Push coverage statistics to coveralls.io 11 | # 12 | 13 | set -e 14 | 15 | workdir=.cover 16 | profile="$workdir/cover.out" 17 | mode=count 18 | 19 | generate_cover_data() { 20 | rm -rf "$workdir" 21 | mkdir "$workdir" 22 | 23 | for pkg in "$@"; do 24 | f="$workdir/$(echo $pkg | tr / -).cover" 25 | go test -covermode="$mode" -coverprofile="$f" "$pkg" 26 | done 27 | 28 | echo "mode: $mode" >"$profile" 29 | grep -h -v "^mode:" "$workdir"/*.cover >>"$profile" 30 | } 31 | 32 | show_cover_report() { 33 | go tool cover -${1}="$profile" 34 | } 35 | 36 | push_to_coveralls() { 37 | echo "Pushing coverage statistics to coveralls.io" 38 | goveralls -coverprofile="$profile" 39 | } 40 | 41 | generate_cover_data $(go list ./...) 42 | show_cover_report func 43 | case "$1" in 44 | "") 45 | ;; 46 | --html) 47 | show_cover_report html ;; 48 | --coveralls) 49 | push_to_coveralls ;; 50 | *) 51 | echo >&2 "error: invalid option: $1"; exit 1 ;; 52 | esac 53 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Priority is a combination of the syslog facility and 8 | // severity. For example, LOG_ALERT | LOG_FTP sends an alert severity 9 | // message from the FTP facility. The default severity is LOG_EMERG; 10 | // the default facility is LOG_KERN. 11 | type Priority int 12 | 13 | const severityMask = 0x07 14 | const facilityMask = 0xf8 15 | 16 | const ( 17 | // Severity. 18 | 19 | // From /usr/include/sys/syslog.h. 20 | // These are the same on Linux, BSD, and OS X. 21 | LOG_EMERG Priority = iota 22 | LOG_ALERT 23 | LOG_CRIT 24 | LOG_ERR 25 | LOG_WARNING 26 | LOG_NOTICE 27 | LOG_INFO 28 | LOG_DEBUG 29 | ) 30 | 31 | const ( 32 | // Facility. 33 | 34 | // From /usr/include/sys/syslog.h. 35 | // These are the same up to LOG_FTP on Linux, BSD, and OS X. 36 | LOG_KERN Priority = iota << 3 37 | LOG_USER 38 | LOG_MAIL 39 | LOG_DAEMON 40 | LOG_AUTH 41 | LOG_SYSLOG 42 | LOG_LPR 43 | LOG_NEWS 44 | LOG_UUCP 45 | LOG_CRON 46 | LOG_AUTHPRIV 47 | LOG_FTP 48 | _ // unused 49 | _ // unused 50 | _ // unused 51 | _ // unused 52 | LOG_LOCAL0 53 | LOG_LOCAL1 54 | LOG_LOCAL2 55 | LOG_LOCAL3 56 | LOG_LOCAL4 57 | LOG_LOCAL5 58 | LOG_LOCAL6 59 | LOG_LOCAL7 60 | ) 61 | 62 | func validatePriority(p Priority) error { 63 | if p < 0 || p > LOG_LOCAL7|LOG_DEBUG { 64 | return errors.New("log/syslog: invalid priority") 65 | } else { 66 | return nil 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Rackspace. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /test/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDr9UU6xYOCGQgm 3 | XOA3HH6ZW/xKATDzxn58nQWXy/1kqI84NFPb9elpWUxU0D86xcU7YZDV1I79N00S 4 | aDTYyHAMLHi6WUt55nokLTRA15mxskRbYE8H5ANKB1+fBDP1LLtGgIhfU2mkfxJp 5 | xtSkcSgmXIi0hstIGzGT98QIsSdUeCpZWF9sKmajGV7wJtzpNcUO8BOh5sd5s37K 6 | 2A5C7w9HZvINPV+/1FDP5GalOhg94eprfV0y78z/+Sxqr7wc2rTw27cjf5waTgQY 7 | gEbzBowMQJP1ojI646vYBrg3TEX7tjnC7f8E+Y7xe614c0Bj8XhJZbNV0MIG8Ka4 8 | 9HlUTcejAgMBAAECggEBAMHtYIuwH6iCOD+HX8QLyET05AJSvk/smLKEPz+GKWlc 9 | W/FemHmUv9SUzvZ5/S2ps7NdObN0slyM4ew59w0gl2558nN9xlmWwlYPTP3p9Oil 10 | 0iajnfCnRsjGDKHdy3I65GRKaUqnfJD020ZSYxwP4Ga+8KAmlNZbe0DYhqZ6Kw/w 11 | 6wwYNvxptjd85Q+jJGj9nyUxL+68LROYNcZga4PQIqNvF1yOvCMIyGKSptfmLRDg 12 | nXAs5vJHPNXdZ5+PC1MmiP2wxC3hZncFVTACoW35++CC6CLmg2OfTOyFZ9FMEQ57 13 | oI99Ed7bjanEioMj8fSk49qIIdFix09ArLa+Rid8fTECgYEA+1w65b5Oq4YU0zXE 14 | C23bgy3D/gZ+vhxCuRU8NLrIKsyFIZuCcA7YU+U/D/ngI52xLEcvSp/NAH/xKHh8 15 | Lop0cKA7oysMxMM4BtEC5N+SquRJKsgg+V36DDNGja4D/4HZ9IlLzTGHL51FI4hd 16 | o489YqgpG0YdCpg5GfoWgQTufIkCgYEA8FBCZQjlFg/cifUewNPQ9mzOz2TyxFTp 17 | olwsqJTnxLgXBJ4N6rdc61bLrWowv7yBHG7/kyMY6s7oWZZCk8EcU682P4DI3X0B 18 | MxuaccMYWc7ttRJEpXWArxiDVCc2sDHVUR23pt0tsSRhQXHlO8HE3679yfG6YGxA 19 | F9WRBct9D8sCgYA6DXMM3IcO1ki4/xHoEddA1LEPWjCrd5txY5YkF39jYxjcSi41 20 | 8zfDKI8IAY3iq+jfcRFbCs0t8F6iGjGUDiYWXOtpI+gvCWdHK76fXYNiNJcxakcz 21 | UKEPcEg7MJV7zWGpOIxpN6chOBFfw37c55gl0PCte+P5Lm8BsODBq4HpAQKBgB2Q 22 | 0kpZ6M1pECoM9UamCLx4sI0Fj3SmOcRW8Mug3k7uky5nP7ET9COkHxTrzqmYSI41 23 | /c2dcNBaum1jNje1d4W4NcVkU9IkMgSWrc63QQSzl71CTR3KMhXYvzeYR3sv9l2v 24 | eUvXRGrZ3flOSPSsJ0uZ3PF+gv6f8ta72MbMvUs3AoGBAPTkm1N56EI4KZIO+uq8 25 | lh094Lytty1MjfhgVf1sRzE+kPxzyo1g2LpnWQ43UZMD5bQ7fR3rr9ofCzPa9OlX 26 | IRXUv4mNFzzDmEZwrrt1TA/EjkosdtqcCg0syvBUEBCkYTdvSxaDCw80sS0DVlEH 27 | gqIra7+MAgb8y+m47tZyO7KZ 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /srslog_unix.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // unixSyslog opens a connection to the syslog daemon running on the 10 | // local machine using a Unix domain socket. This function exists because of 11 | // Solaris support as implemented by gccgo. On Solaris you can not 12 | // simply open a TCP connection to the syslog daemon. The gccgo 13 | // sources have a syslog_solaris.go file that implements unixSyslog to 14 | // return a type that satisfies the serverConn interface and simply calls the C 15 | // library syslog function. 16 | func unixSyslog() (conn serverConn, err error) { 17 | logTypes := []string{"unixgram", "unix"} 18 | logPaths := []string{"/dev/log", "/var/run/syslog", "/var/run/log"} 19 | for _, network := range logTypes { 20 | for _, path := range logPaths { 21 | conn, err := net.Dial(network, path) 22 | if err != nil { 23 | continue 24 | } else { 25 | return &localConn{conn: conn}, nil 26 | } 27 | } 28 | } 29 | return nil, errors.New("Unix syslog delivery error") 30 | } 31 | 32 | // localConn adheres to the serverConn interface, allowing us to send syslog 33 | // messages to the local syslog daemon over a Unix domain socket. 34 | type localConn struct { 35 | conn io.WriteCloser 36 | } 37 | 38 | // writeString formats syslog messages using time.Stamp instead of time.RFC3339, 39 | // and omits the hostname (because it is expected to be used locally). 40 | func (n *localConn) writeString(framer Framer, formatter Formatter, p Priority, hostname, tag, msg string) error { 41 | if framer == nil { 42 | framer = DefaultFramer 43 | } 44 | if formatter == nil { 45 | formatter = UnixFormatter 46 | } 47 | _, err := n.conn.Write([]byte(framer(formatter(p, hostname, tag, msg)))) 48 | return err 49 | } 50 | 51 | // close the (local) network connection 52 | func (n *localConn) close() error { 53 | return n.conn.Close() 54 | } 55 | -------------------------------------------------------------------------------- /formatter_test.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | "strings" 9 | ) 10 | 11 | func TestDefaultFormatter(t *testing.T) { 12 | out := DefaultFormatter(LOG_ERR, "hostname", "tag", "content") 13 | expected := fmt.Sprintf("<%d> %s %s %s[%d]: %s", 14 | LOG_ERR, time.Now().Format(time.RFC3339), "hostname", "tag", os.Getpid(), "content") 15 | if out != expected { 16 | t.Errorf("expected %v got %v", expected, out) 17 | } 18 | } 19 | 20 | func TestUnixFormatter(t *testing.T) { 21 | out := UnixFormatter(LOG_ERR, "hostname", "tag", "content") 22 | expected := fmt.Sprintf("<%d>%s %s[%d]: %s", 23 | LOG_ERR, time.Now().Format(time.Stamp), "tag", os.Getpid(), "content") 24 | if out != expected { 25 | t.Errorf("expected %v got %v", expected, out) 26 | } 27 | } 28 | 29 | func TestRFC3164Formatter(t *testing.T) { 30 | out := RFC3164Formatter(LOG_ERR, "hostname", "tag", "content") 31 | expected := fmt.Sprintf("<%d>%s %s %s[%d]: %s", 32 | LOG_ERR, time.Now().Format(time.Stamp), "hostname", "tag", os.Getpid(), "content") 33 | if out != expected { 34 | t.Errorf("expected %v got %v", expected, out) 35 | } 36 | } 37 | 38 | func TestRFC5424Formatter(t *testing.T) { 39 | out := RFC5424Formatter(LOG_ERR, "hostname", "tag", "content") 40 | expected := fmt.Sprintf("<%d>%d %s %s %s %d %s - %s", 41 | LOG_ERR, 1, time.Now().Format(time.RFC3339), "hostname", truncateStartStr(os.Args[0], appNameMaxLength), 42 | os.Getpid(), "tag", "content") 43 | if out != expected { 44 | t.Errorf("expected %v got %v", expected, out) 45 | } 46 | } 47 | 48 | func TestTruncateStartStr(t *testing.T) { 49 | out := truncateStartStr("abcde", 3) 50 | if strings.Compare(out, "cde" ) != 0 { 51 | t.Errorf("expected \"cde\" got %v", out) 52 | } 53 | out = truncateStartStr("abcde", 5) 54 | if strings.Compare(out, "abcde" ) != 0 { 55 | t.Errorf("expected \"abcde\" got %v", out) 56 | } 57 | } -------------------------------------------------------------------------------- /script/gen-certs.py: -------------------------------------------------------------------------------- 1 | # To run this, make a virtualenv and pip install cryptography. 2 | # If you're using virtualenvwrapper you can just mktmpenv 3 | 4 | import datetime 5 | import ipaddress 6 | import uuid 7 | 8 | from cryptography import x509 9 | from cryptography.hazmat.backends import default_backend 10 | from cryptography.hazmat.primitives import hashes, serialization 11 | from cryptography.hazmat.primitives.asymmetric import rsa 12 | from cryptography.x509.oid import NameOID 13 | 14 | 15 | one_day = datetime.timedelta(1, 0, 0) 16 | private_key = rsa.generate_private_key( 17 | public_exponent=65537, 18 | key_size=2048, 19 | backend=default_backend() 20 | ) 21 | builder = x509.CertificateBuilder() 22 | builder = builder.subject_name(x509.Name([ 23 | x509.NameAttribute(NameOID.COMMON_NAME, u'127.0.0.1'), 24 | ])) 25 | builder = builder.issuer_name(x509.Name([ 26 | x509.NameAttribute(NameOID.COMMON_NAME, u'127.0.0.1'), 27 | ])) 28 | builder = builder.not_valid_before(datetime.datetime.today() - one_day) 29 | builder = builder.not_valid_after(datetime.datetime(2018, 12, 31)) 30 | builder = builder.serial_number(int(uuid.uuid4())) 31 | builder = builder.public_key(private_key.public_key()) 32 | builder = builder.add_extension( 33 | x509.SubjectAlternativeName([ 34 | x509.IPAddress(ipaddress.IPv4Address(u'127.0.0.1')) 35 | ]), 36 | critical=False, 37 | ) 38 | builder = builder.add_extension( 39 | x509.KeyUsage(True, False, True, False, True, True, True, False, False), 40 | critical=False, 41 | ) 42 | builder = builder.add_extension( 43 | x509.BasicConstraints(ca=True, path_length=None), 44 | critical=False, 45 | ) 46 | certificate = builder.sign( 47 | private_key=private_key, algorithm=hashes.SHA256(), 48 | backend=default_backend() 49 | ) 50 | print(certificate.public_bytes(serialization.Encoding.PEM)) 51 | print(private_key.private_bytes( 52 | serialization.Encoding.PEM, 53 | serialization.PrivateFormat.PKCS8, 54 | serialization.NoEncryption() 55 | )) 56 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | const appNameMaxLength = 48 // limit to 48 chars as per RFC5424 10 | 11 | // Formatter is a type of function that takes the consituent parts of a 12 | // syslog message and returns a formatted string. A different Formatter is 13 | // defined for each different syslog protocol we support. 14 | type Formatter func(p Priority, hostname, tag, content string) string 15 | 16 | // DefaultFormatter is the original format supported by the Go syslog package, 17 | // and is a non-compliant amalgamation of 3164 and 5424 that is intended to 18 | // maximize compatibility. 19 | func DefaultFormatter(p Priority, hostname, tag, content string) string { 20 | timestamp := time.Now().Format(time.RFC3339) 21 | msg := fmt.Sprintf("<%d> %s %s %s[%d]: %s", 22 | p, timestamp, hostname, tag, os.Getpid(), content) 23 | return msg 24 | } 25 | 26 | // UnixFormatter omits the hostname, because it is only used locally. 27 | func UnixFormatter(p Priority, hostname, tag, content string) string { 28 | timestamp := time.Now().Format(time.Stamp) 29 | msg := fmt.Sprintf("<%d>%s %s[%d]: %s", 30 | p, timestamp, tag, os.Getpid(), content) 31 | return msg 32 | } 33 | 34 | // RFC3164Formatter provides an RFC 3164 compliant message. 35 | func RFC3164Formatter(p Priority, hostname, tag, content string) string { 36 | timestamp := time.Now().Format(time.Stamp) 37 | msg := fmt.Sprintf("<%d>%s %s %s[%d]: %s", 38 | p, timestamp, hostname, tag, os.Getpid(), content) 39 | return msg 40 | } 41 | 42 | // if string's length is greater than max, then use the last part 43 | func truncateStartStr(s string, max int) string { 44 | if (len(s) > max) { 45 | return s[len(s) - max:] 46 | } 47 | return s 48 | } 49 | 50 | // RFC5424Formatter provides an RFC 5424 compliant message. 51 | func RFC5424Formatter(p Priority, hostname, tag, content string) string { 52 | timestamp := time.Now().Format(time.RFC3339) 53 | pid := os.Getpid() 54 | appName := truncateStartStr(os.Args[0], appNameMaxLength) 55 | msg := fmt.Sprintf("<%d>%d %s %s %s %d %s - %s", 56 | p, 1, timestamp, hostname, appName, pid, tag, content) 57 | return msg 58 | } 59 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at [sirsean@gmail.com]. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /dialer.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | ) 7 | 8 | // dialerFunctionWrapper is a simple object that consists of a dialer function 9 | // and its name. This is primarily for testing, so we can make sure that the 10 | // getDialer method returns the correct dialer function. However, if you ever 11 | // find that you need to check which dialer function you have, this would also 12 | // be useful for you without having to use reflection. 13 | type dialerFunctionWrapper struct { 14 | Name string 15 | Dialer func() (serverConn, string, error) 16 | } 17 | 18 | // Call the wrapped dialer function and return its return values. 19 | func (df dialerFunctionWrapper) Call() (serverConn, string, error) { 20 | return df.Dialer() 21 | } 22 | 23 | // getDialer returns a "dialer" function that can be called to connect to a 24 | // syslog server. 25 | // 26 | // Each dialer function is responsible for dialing the remote host and returns 27 | // a serverConn, the hostname (or a default if the Writer has not specified a 28 | // hostname), and an error in case dialing fails. 29 | // 30 | // The reason for separate dialers is that different network types may need 31 | // to dial their connection differently, yet still provide a net.Conn interface 32 | // that you can use once they have dialed. Rather than an increasingly long 33 | // conditional, we have a map of network -> dialer function (with a sane default 34 | // value), and adding a new network type is as easy as writing the dialer 35 | // function and adding it to the map. 36 | func (w *Writer) getDialer() dialerFunctionWrapper { 37 | dialers := map[string]dialerFunctionWrapper{ 38 | "": dialerFunctionWrapper{"unixDialer", w.unixDialer}, 39 | "tcp+tls": dialerFunctionWrapper{"tlsDialer", w.tlsDialer}, 40 | "custom": dialerFunctionWrapper{"customDialer", w.customDialer}, 41 | } 42 | dialer, ok := dialers[w.network] 43 | if !ok { 44 | dialer = dialerFunctionWrapper{"basicDialer", w.basicDialer} 45 | } 46 | return dialer 47 | } 48 | 49 | // unixDialer uses the unixSyslog method to open a connection to the syslog 50 | // daemon running on the local machine. 51 | func (w *Writer) unixDialer() (serverConn, string, error) { 52 | sc, err := unixSyslog() 53 | hostname := w.hostname 54 | if hostname == "" { 55 | hostname = "localhost" 56 | } 57 | return sc, hostname, err 58 | } 59 | 60 | // tlsDialer connects to TLS over TCP, and is used for the "tcp+tls" network 61 | // type. 62 | func (w *Writer) tlsDialer() (serverConn, string, error) { 63 | c, err := tls.Dial("tcp", w.raddr, w.tlsConfig) 64 | var sc serverConn 65 | hostname := w.hostname 66 | if err == nil { 67 | sc = &netConn{conn: c} 68 | if hostname == "" { 69 | hostname = c.LocalAddr().String() 70 | } 71 | } 72 | return sc, hostname, err 73 | } 74 | 75 | // basicDialer is the most common dialer for syslog, and supports both TCP and 76 | // UDP connections. 77 | func (w *Writer) basicDialer() (serverConn, string, error) { 78 | c, err := net.Dial(w.network, w.raddr) 79 | var sc serverConn 80 | hostname := w.hostname 81 | if err == nil { 82 | sc = &netConn{conn: c} 83 | if hostname == "" { 84 | hostname = c.LocalAddr().String() 85 | } 86 | } 87 | return sc, hostname, err 88 | } 89 | 90 | // customDialer uses the custom dialer when the Writer was created 91 | // giving developers total control over how connections are made and returned. 92 | // Note it does not check if cdialer is nil, as it should only be referenced from getDialer. 93 | func (w *Writer) customDialer() (serverConn, string, error) { 94 | c, err := w.customDial(w.network, w.raddr) 95 | var sc serverConn 96 | hostname := w.hostname 97 | if err == nil { 98 | sc = &netConn{conn: c} 99 | if hostname == "" { 100 | hostname = c.LocalAddr().String() 101 | } 102 | } 103 | return sc, hostname, err 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/RackSec/srslog.svg?branch=master)](https://travis-ci.org/RackSec/srslog) 2 | 3 | # srslog 4 | 5 | Go has a `syslog` package in the standard library, but it has the following 6 | shortcomings: 7 | 8 | 1. It doesn't have TLS support 9 | 2. [According to bradfitz on the Go team, it is no longer being maintained.](https://github.com/golang/go/issues/13449#issuecomment-161204716) 10 | 11 | I agree that it doesn't need to be in the standard library. So, I've 12 | followed Brad's suggestion and have made a separate project to handle syslog. 13 | 14 | This code was taken directly from the Go project as a base to start from. 15 | 16 | However, this _does_ have TLS support. 17 | 18 | # Usage 19 | 20 | Basic usage retains the same interface as the original `syslog` package. We 21 | only added to the interface where required to support new functionality. 22 | 23 | Switch from the standard library: 24 | 25 | ``` 26 | import( 27 | //"log/syslog" 28 | syslog "github.com/RackSec/srslog" 29 | ) 30 | ``` 31 | 32 | You can still use it for local syslog: 33 | 34 | ``` 35 | w, err := syslog.Dial("", "", syslog.LOG_ERR, "testtag") 36 | ``` 37 | 38 | Or to unencrypted UDP: 39 | 40 | ``` 41 | w, err := syslog.Dial("udp", "192.168.0.50:514", syslog.LOG_ERR, "testtag") 42 | ``` 43 | 44 | Or to unencrypted TCP: 45 | 46 | ``` 47 | w, err := syslog.Dial("tcp", "192.168.0.51:514", syslog.LOG_ERR, "testtag") 48 | ``` 49 | 50 | But now you can also send messages via TLS-encrypted TCP: 51 | 52 | ``` 53 | w, err := syslog.DialWithTLSCertPath("tcp+tls", "192.168.0.52:514", syslog.LOG_ERR, "testtag", "/path/to/servercert.pem") 54 | ``` 55 | 56 | And if you need more control over your TLS configuration : 57 | 58 | ``` 59 | pool := x509.NewCertPool() 60 | serverCert, err := ioutil.ReadFile("/path/to/servercert.pem") 61 | if err != nil { 62 | return nil, err 63 | } 64 | pool.AppendCertsFromPEM(serverCert) 65 | config := tls.Config{ 66 | RootCAs: pool, 67 | } 68 | 69 | w, err := DialWithTLSConfig(network, raddr, priority, tag, &config) 70 | ``` 71 | 72 | (Note that in both TLS cases, this uses a self-signed certificate, where the 73 | remote syslog server has the keypair and the client has only the public key.) 74 | 75 | And then to write log messages, continue like so: 76 | 77 | ``` 78 | if err != nil { 79 | log.Fatal("failed to connect to syslog:", err) 80 | } 81 | defer w.Close() 82 | 83 | w.Alert("this is an alert") 84 | w.Crit("this is critical") 85 | w.Err("this is an error") 86 | w.Warning("this is a warning") 87 | w.Notice("this is a notice") 88 | w.Info("this is info") 89 | w.Debug("this is debug") 90 | w.Write([]byte("these are some bytes")) 91 | ``` 92 | 93 | If you need further control over connection attempts, you can use the DialWithCustomDialer 94 | function. To continue with the DialWithTLSConfig example: 95 | 96 | ``` 97 | netDialer := &net.Dialer{Timeout: time.Second*5} // easy timeouts 98 | realNetwork := "tcp" // real network, other vars your dail func can close over 99 | dial := func(network, addr string) (net.Conn, error) { 100 | // cannot use "network" here as it'll simply be "custom" which will fail 101 | return tls.DialWithDialer(netDialer, realNetwork, addr, &config) 102 | } 103 | 104 | w, err := DialWithCustomDialer("custom", "192.168.0.52:514", syslog.LOG_ERR, "testtag", dial) 105 | ``` 106 | 107 | Your custom dial func can set timeouts, proxy connections, and do whatever else it needs before returning a net.Conn. 108 | 109 | # Generating TLS Certificates 110 | 111 | We've provided a script that you can use to generate a self-signed keypair: 112 | 113 | ``` 114 | pip install cryptography 115 | python script/gen-certs.py 116 | ``` 117 | 118 | That outputs the public key and private key to standard out. Put those into 119 | `.pem` files. (And don't put them into any source control. The certificate in 120 | the `test` directory is used by the unit tests, and please do not actually use 121 | it anywhere else.) 122 | 123 | # Running Tests 124 | 125 | Run the tests as usual: 126 | 127 | ``` 128 | go test 129 | ``` 130 | 131 | But we've also provided a test coverage script that will show you which 132 | lines of code are not covered: 133 | 134 | ``` 135 | script/coverage --html 136 | ``` 137 | 138 | That will open a new browser tab showing coverage information. 139 | 140 | # License 141 | 142 | This project uses the New BSD License, the same as the Go project itself. 143 | 144 | # Code of Conduct 145 | 146 | Please note that this project is released with a Contributor Code of Conduct. 147 | By participating in this project you agree to abide by its terms. 148 | -------------------------------------------------------------------------------- /srslog.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "os" 11 | ) 12 | 13 | // This interface allows us to work with both local and network connections, 14 | // and enables Solaris support (see syslog_unix.go). 15 | type serverConn interface { 16 | writeString(framer Framer, formatter Formatter, p Priority, hostname, tag, s string) error 17 | close() error 18 | } 19 | 20 | // DialFunc is the function signature to be used for a custom dialer callback 21 | // with DialWithCustomDialer 22 | type DialFunc func(string, string) (net.Conn, error) 23 | 24 | // New establishes a new connection to the system log daemon. Each 25 | // write to the returned Writer sends a log message with the given 26 | // priority and prefix. 27 | func New(priority Priority, tag string) (w *Writer, err error) { 28 | return Dial("", "", priority, tag) 29 | } 30 | 31 | // Dial establishes a connection to a log daemon by connecting to 32 | // address raddr on the specified network. Each write to the returned 33 | // Writer sends a log message with the given facility, severity and 34 | // tag. 35 | // If network is empty, Dial will connect to the local syslog server. 36 | func Dial(network, raddr string, priority Priority, tag string) (*Writer, error) { 37 | return DialWithTLSConfig(network, raddr, priority, tag, nil) 38 | } 39 | 40 | // ErrNilDialFunc is returned from DialWithCustomDialer when a nil DialFunc is passed, 41 | // avoiding a nil pointer deference panic. 42 | var ErrNilDialFunc = errors.New("srslog: nil DialFunc passed to DialWithCustomDialer") 43 | 44 | // DialWithCustomDialer establishes a connection by calling customDial. 45 | // Each write to the returned Writer sends a log message with the given facility, severity and tag. 46 | // Network must be "custom" in order for this package to use customDial. 47 | // While network and raddr will be passed to customDial, it is allowed for customDial to ignore them. 48 | // If customDial is nil, this function returns ErrNilDialFunc. 49 | func DialWithCustomDialer(network, raddr string, priority Priority, tag string, customDial DialFunc) (*Writer, error) { 50 | if customDial == nil { 51 | return nil, ErrNilDialFunc 52 | } 53 | return dialAllParameters(network, raddr, priority, tag, nil, customDial) 54 | } 55 | 56 | // DialWithTLSCertPath establishes a secure connection to a log daemon by connecting to 57 | // address raddr on the specified network. It uses certPath to load TLS certificates and configure 58 | // the secure connection. 59 | func DialWithTLSCertPath(network, raddr string, priority Priority, tag, certPath string) (*Writer, error) { 60 | serverCert, err := ioutil.ReadFile(certPath) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return DialWithTLSCert(network, raddr, priority, tag, serverCert) 66 | } 67 | 68 | // DialWIthTLSCert establishes a secure connection to a log daemon by connecting to 69 | // address raddr on the specified network. It uses serverCert to load a TLS certificate 70 | // and configure the secure connection. 71 | func DialWithTLSCert(network, raddr string, priority Priority, tag string, serverCert []byte) (*Writer, error) { 72 | pool := x509.NewCertPool() 73 | pool.AppendCertsFromPEM(serverCert) 74 | config := tls.Config{ 75 | RootCAs: pool, 76 | } 77 | 78 | return DialWithTLSConfig(network, raddr, priority, tag, &config) 79 | } 80 | 81 | // DialWithTLSConfig establishes a secure connection to a log daemon by connecting to 82 | // address raddr on the specified network. It uses tlsConfig to configure the secure connection. 83 | func DialWithTLSConfig(network, raddr string, priority Priority, tag string, tlsConfig *tls.Config) (*Writer, error) { 84 | return dialAllParameters(network, raddr, priority, tag, tlsConfig, nil) 85 | } 86 | 87 | // implementation of the various functions above 88 | func dialAllParameters(network, raddr string, priority Priority, tag string, tlsConfig *tls.Config, customDial DialFunc) (*Writer, error) { 89 | if err := validatePriority(priority); err != nil { 90 | return nil, err 91 | } 92 | 93 | if tag == "" { 94 | tag = os.Args[0] 95 | } 96 | hostname, _ := os.Hostname() 97 | 98 | w := &Writer{ 99 | priority: priority, 100 | tag: tag, 101 | hostname: hostname, 102 | network: network, 103 | raddr: raddr, 104 | tlsConfig: tlsConfig, 105 | customDial: customDial, 106 | } 107 | 108 | _, err := w.connect() 109 | if err != nil { 110 | return nil, err 111 | } 112 | return w, err 113 | } 114 | 115 | // NewLogger creates a log.Logger whose output is written to 116 | // the system log service with the specified priority. The logFlag 117 | // argument is the flag set passed through to log.New to create 118 | // the Logger. 119 | func NewLogger(p Priority, logFlag int) (*log.Logger, error) { 120 | s, err := New(p, "") 121 | if err != nil { 122 | return nil, err 123 | } 124 | return log.New(s, "", logFlag), nil 125 | } 126 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "crypto/tls" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | // A Writer is a connection to a syslog server. 10 | type Writer struct { 11 | priority Priority 12 | tag string 13 | hostname string 14 | network string 15 | raddr string 16 | tlsConfig *tls.Config 17 | framer Framer 18 | formatter Formatter 19 | 20 | //non-nil if custom dialer set, used in getDialer 21 | customDial DialFunc 22 | 23 | mu sync.RWMutex // guards conn 24 | conn serverConn 25 | } 26 | 27 | // getConn provides access to the internal conn, protected by a mutex. The 28 | // conn is threadsafe, so it can be used while unlocked, but we want to avoid 29 | // race conditions on grabbing a reference to it. 30 | func (w *Writer) getConn() serverConn { 31 | w.mu.RLock() 32 | conn := w.conn 33 | w.mu.RUnlock() 34 | return conn 35 | } 36 | 37 | // setConn updates the internal conn, protected by a mutex. 38 | func (w *Writer) setConn(c serverConn) { 39 | w.mu.Lock() 40 | w.conn = c 41 | w.mu.Unlock() 42 | } 43 | 44 | // connect makes a connection to the syslog server. 45 | func (w *Writer) connect() (serverConn, error) { 46 | conn := w.getConn() 47 | if conn != nil { 48 | // ignore err from close, it makes sense to continue anyway 49 | conn.close() 50 | w.setConn(nil) 51 | } 52 | 53 | var hostname string 54 | var err error 55 | dialer := w.getDialer() 56 | conn, hostname, err = dialer.Call() 57 | if err == nil { 58 | w.setConn(conn) 59 | w.hostname = hostname 60 | 61 | return conn, nil 62 | } else { 63 | return nil, err 64 | } 65 | } 66 | 67 | // SetFormatter changes the formatter function for subsequent messages. 68 | func (w *Writer) SetFormatter(f Formatter) { 69 | w.formatter = f 70 | } 71 | 72 | // SetFramer changes the framer function for subsequent messages. 73 | func (w *Writer) SetFramer(f Framer) { 74 | w.framer = f 75 | } 76 | 77 | // SetHostname changes the hostname for syslog messages if needed. 78 | func (w *Writer) SetHostname(hostname string) { 79 | w.hostname = hostname 80 | } 81 | 82 | // Write sends a log message to the syslog daemon using the default priority 83 | // passed into `srslog.New` or the `srslog.Dial*` functions. 84 | func (w *Writer) Write(b []byte) (int, error) { 85 | return w.writeAndRetry(w.priority, string(b)) 86 | } 87 | 88 | // WriteWithPriority sends a log message with a custom priority. 89 | func (w *Writer) WriteWithPriority(p Priority, b []byte) (int, error) { 90 | return w.writeAndRetryWithPriority(p, string(b)) 91 | } 92 | 93 | // Close closes a connection to the syslog daemon. 94 | func (w *Writer) Close() error { 95 | conn := w.getConn() 96 | if conn != nil { 97 | err := conn.close() 98 | w.setConn(nil) 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | // Emerg logs a message with severity LOG_EMERG; this overrides the default 105 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 106 | func (w *Writer) Emerg(m string) (err error) { 107 | _, err = w.writeAndRetry(LOG_EMERG, m) 108 | return err 109 | } 110 | 111 | // Alert logs a message with severity LOG_ALERT; this overrides the default 112 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 113 | func (w *Writer) Alert(m string) (err error) { 114 | _, err = w.writeAndRetry(LOG_ALERT, m) 115 | return err 116 | } 117 | 118 | // Crit logs a message with severity LOG_CRIT; this overrides the default 119 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 120 | func (w *Writer) Crit(m string) (err error) { 121 | _, err = w.writeAndRetry(LOG_CRIT, m) 122 | return err 123 | } 124 | 125 | // Err logs a message with severity LOG_ERR; this overrides the default 126 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 127 | func (w *Writer) Err(m string) (err error) { 128 | _, err = w.writeAndRetry(LOG_ERR, m) 129 | return err 130 | } 131 | 132 | // Warning logs a message with severity LOG_WARNING; this overrides the default 133 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 134 | func (w *Writer) Warning(m string) (err error) { 135 | _, err = w.writeAndRetry(LOG_WARNING, m) 136 | return err 137 | } 138 | 139 | // Notice logs a message with severity LOG_NOTICE; this overrides the default 140 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 141 | func (w *Writer) Notice(m string) (err error) { 142 | _, err = w.writeAndRetry(LOG_NOTICE, m) 143 | return err 144 | } 145 | 146 | // Info logs a message with severity LOG_INFO; this overrides the default 147 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 148 | func (w *Writer) Info(m string) (err error) { 149 | _, err = w.writeAndRetry(LOG_INFO, m) 150 | return err 151 | } 152 | 153 | // Debug logs a message with severity LOG_DEBUG; this overrides the default 154 | // priority passed to `srslog.New` and the `srslog.Dial*` functions. 155 | func (w *Writer) Debug(m string) (err error) { 156 | _, err = w.writeAndRetry(LOG_DEBUG, m) 157 | return err 158 | } 159 | 160 | // writeAndRetry takes a severity and the string to write. Any facility passed to 161 | // it as part of the severity Priority will be ignored. 162 | func (w *Writer) writeAndRetry(severity Priority, s string) (int, error) { 163 | pr := (w.priority & facilityMask) | (severity & severityMask) 164 | 165 | return w.writeAndRetryWithPriority(pr, s) 166 | } 167 | 168 | // writeAndRetryWithPriority differs from writeAndRetry in that it allows setting 169 | // of both the facility and the severity. 170 | func (w *Writer) writeAndRetryWithPriority(p Priority, s string) (int, error) { 171 | conn := w.getConn() 172 | if conn != nil { 173 | if n, err := w.write(conn, p, s); err == nil { 174 | return n, err 175 | } 176 | } 177 | 178 | var err error 179 | if conn, err = w.connect(); err != nil { 180 | return 0, err 181 | } 182 | return w.write(conn, p, s) 183 | } 184 | 185 | // write generates and writes a syslog formatted string. It formats the 186 | // message based on the current Formatter and Framer. 187 | func (w *Writer) write(conn serverConn, p Priority, msg string) (int, error) { 188 | // ensure it ends in a \n 189 | if !strings.HasSuffix(msg, "\n") { 190 | msg += "\n" 191 | } 192 | 193 | err := conn.writeString(w.framer, w.formatter, p, w.hostname, w.tag, msg) 194 | if err != nil { 195 | return 0, err 196 | } 197 | // Note: return the length of the input, not the number of 198 | // bytes printed by Fprintf, because this must behave like 199 | // an io.Writer. 200 | return len(msg), nil 201 | } 202 | -------------------------------------------------------------------------------- /dialer_test.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "io/ioutil" 8 | "net" 9 | "testing" 10 | ) 11 | 12 | func TestGetDialer(t *testing.T) { 13 | w := Writer{ 14 | priority: LOG_ERR, 15 | tag: "tag", 16 | hostname: "", 17 | network: "", 18 | raddr: "", 19 | } 20 | 21 | dialer := w.getDialer() 22 | if "unixDialer" != dialer.Name { 23 | t.Errorf("should get unixDialer, got: %v", dialer) 24 | } 25 | 26 | w.network = "tcp+tls" 27 | dialer = w.getDialer() 28 | if "tlsDialer" != dialer.Name { 29 | t.Errorf("should get tlsDialer, got: %v", dialer) 30 | } 31 | 32 | w.network = "tcp" 33 | dialer = w.getDialer() 34 | if "basicDialer" != dialer.Name { 35 | t.Errorf("should get basicDialer, got: %v", dialer) 36 | } 37 | 38 | w.network = "udp" 39 | dialer = w.getDialer() 40 | if "basicDialer" != dialer.Name { 41 | t.Errorf("should get basicDialer, got: %v", dialer) 42 | } 43 | 44 | w.network = "something else entirely" 45 | dialer = w.getDialer() 46 | if "basicDialer" != dialer.Name { 47 | t.Errorf("should get basicDialer, got: %v", dialer) 48 | } 49 | 50 | w.network = "custom" 51 | w.customDial = func(string, string) (net.Conn, error) { return nil, nil } 52 | dialer = w.getDialer() 53 | if "customDialer" != dialer.Name { 54 | t.Errorf("should get customDialer, got: %v", dialer) 55 | } 56 | } 57 | 58 | func TestUnixDialer(t *testing.T) { 59 | w := Writer{ 60 | priority: LOG_ERR, 61 | tag: "tag", 62 | hostname: "", 63 | network: "", 64 | raddr: "", 65 | } 66 | 67 | _, hostname, err := w.unixDialer() 68 | 69 | if err != nil { 70 | t.Errorf("failed to dial: %v", err) 71 | } 72 | 73 | if hostname != "localhost" { 74 | t.Errorf("should set blank hostname") 75 | } 76 | 77 | w.hostname = "my other hostname" 78 | 79 | _, hostname, err = w.unixDialer() 80 | 81 | if err != nil { 82 | t.Errorf("failed to dial: %v", err) 83 | } 84 | 85 | if hostname != "my other hostname" { 86 | t.Errorf("should not interfere with hostname") 87 | } 88 | } 89 | 90 | func TestTLSDialer(t *testing.T) { 91 | done := make(chan string) 92 | addr, sock, _ := startServer("tcp+tls", "", done) 93 | defer sock.Close() 94 | 95 | pool := x509.NewCertPool() 96 | serverCert, err := ioutil.ReadFile("test/cert.pem") 97 | if err != nil { 98 | t.Errorf("failed to read file: %v", err) 99 | } 100 | pool.AppendCertsFromPEM(serverCert) 101 | config := tls.Config{ 102 | RootCAs: pool, 103 | } 104 | 105 | w := Writer{ 106 | priority: LOG_ERR, 107 | tag: "tag", 108 | hostname: "", 109 | network: "tcp+tls", 110 | raddr: addr, 111 | tlsConfig: &config, 112 | } 113 | 114 | _, hostname, err := w.tlsDialer() 115 | 116 | if err != nil { 117 | t.Errorf("failed to dial: %v", err) 118 | } 119 | 120 | if hostname == "" { 121 | t.Errorf("should set default hostname") 122 | } 123 | 124 | w.hostname = "my other hostname" 125 | 126 | _, hostname, err = w.tlsDialer() 127 | 128 | if err != nil { 129 | t.Errorf("failed to dial: %v", err) 130 | } 131 | 132 | if hostname != "my other hostname" { 133 | t.Errorf("should not interfere with hostname") 134 | } 135 | } 136 | 137 | func TestTCPDialer(t *testing.T) { 138 | done := make(chan string) 139 | addr, sock, _ := startServer("tcp", "", done) 140 | defer sock.Close() 141 | 142 | w := Writer{ 143 | priority: LOG_ERR, 144 | tag: "tag", 145 | hostname: "", 146 | network: "tcp", 147 | raddr: addr, 148 | } 149 | 150 | _, hostname, err := w.basicDialer() 151 | 152 | if err != nil { 153 | t.Errorf("failed to dial: %v", err) 154 | } 155 | 156 | if hostname == "" { 157 | t.Errorf("should set default hostname") 158 | } 159 | 160 | w.hostname = "my other hostname" 161 | 162 | _, hostname, err = w.basicDialer() 163 | 164 | if err != nil { 165 | t.Errorf("failed to dial: %v", err) 166 | } 167 | 168 | if hostname != "my other hostname" { 169 | t.Errorf("should not interfere with hostname") 170 | } 171 | } 172 | 173 | func TestUDPDialer(t *testing.T) { 174 | done := make(chan string) 175 | addr, sock, _ := startServer("udp", "", done) 176 | defer sock.Close() 177 | 178 | w := Writer{ 179 | priority: LOG_ERR, 180 | tag: "tag", 181 | hostname: "", 182 | network: "udp", 183 | raddr: addr, 184 | } 185 | 186 | _, hostname, err := w.basicDialer() 187 | 188 | if err != nil { 189 | t.Errorf("failed to dial: %v", err) 190 | } 191 | 192 | if hostname == "" { 193 | t.Errorf("should set default hostname") 194 | } 195 | 196 | w.hostname = "my other hostname" 197 | 198 | _, hostname, err = w.basicDialer() 199 | 200 | if err != nil { 201 | t.Errorf("failed to dial: %v", err) 202 | } 203 | 204 | if hostname != "my other hostname" { 205 | t.Errorf("should not interfere with hostname") 206 | } 207 | } 208 | 209 | func TestCustomDialer(t *testing.T) { 210 | // A custom dialer can really be anything, so we don't test an actual connection 211 | // instead we test the behavior of this code path 212 | 213 | nwork, addr := "custom", "custom_addr_to_pass" 214 | w := Writer{ 215 | priority: LOG_ERR, 216 | tag: "tag", 217 | hostname: "", 218 | network: nwork, 219 | raddr: addr, 220 | customDial: func(n string, a string) (net.Conn, error) { 221 | if n != nwork || a != addr { 222 | return nil, errors.New("Unexpected network or address, expected: (" + 223 | nwork + ":" + addr + ") but received (" + n + ":" + a + ")") 224 | } 225 | return fakeConn{addr: &fakeAddr{nwork, addr}}, nil 226 | }, 227 | } 228 | 229 | _, hostname, err := w.customDialer() 230 | 231 | if err != nil { 232 | t.Errorf("failed to dial: %v", err) 233 | } 234 | 235 | if hostname == "" { 236 | t.Errorf("should set default hostname") 237 | } 238 | 239 | w.hostname = "my other hostname" 240 | 241 | _, hostname, err = w.customDialer() 242 | 243 | if err != nil { 244 | t.Errorf("failed to dial: %v", err) 245 | } 246 | 247 | if hostname != "my other hostname" { 248 | t.Errorf("should not interfere with hostname") 249 | } 250 | } 251 | 252 | type fakeConn struct { 253 | net.Conn 254 | addr net.Addr 255 | } 256 | 257 | func (fc fakeConn) Close() error { 258 | return nil 259 | } 260 | 261 | func (fc fakeConn) Write(p []byte) (int, error) { 262 | return len(p), nil 263 | } 264 | 265 | func (fc fakeConn) LocalAddr() net.Addr { 266 | return fc.addr 267 | } 268 | 269 | type fakeAddr struct { 270 | nwork, addr string 271 | } 272 | 273 | func (fa *fakeAddr) Network() string { 274 | return fa.nwork 275 | } 276 | 277 | func (fa *fakeAddr) String() string { 278 | return fa.addr 279 | } 280 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestCloseNonOpenWriter(t *testing.T) { 9 | w := Writer{} 10 | 11 | err := w.Close() 12 | if err != nil { 13 | t.Errorf("should not fail to close if there is nothing to close") 14 | } 15 | } 16 | 17 | func TestWriteAndRetryFails(t *testing.T) { 18 | w := Writer{network: "udp", raddr: "fakehost"} 19 | 20 | n, err := w.writeAndRetry(LOG_ERR, "nope") 21 | if err == nil { 22 | t.Errorf("should fail to write") 23 | } 24 | if n != 0 { 25 | t.Errorf("should not write any bytes") 26 | } 27 | } 28 | 29 | func TestSetHostname(t *testing.T) { 30 | customHostname := "kubernetesCluster" 31 | expected := customHostname 32 | 33 | done := make(chan string) 34 | addr, sock, srvWG := startServer("udp", "", done) 35 | defer sock.Close() 36 | defer srvWG.Wait() 37 | 38 | w := Writer{ 39 | priority: LOG_ERR, 40 | network: "udp", 41 | raddr: addr, 42 | } 43 | 44 | _, err := w.connect() 45 | if err != nil { 46 | t.Errorf("failed to connect: %v", err) 47 | } 48 | defer w.Close() 49 | 50 | if strings.Split(w.hostname, ":")[0] != "127.0.0.1" { 51 | t.Errorf("expected hostname: %s, got %s", "127.0.0.1", strings.Split(w.hostname, ":")[0]) 52 | } 53 | 54 | w.SetHostname(customHostname) 55 | if w.hostname != expected { 56 | t.Errorf("expected hostname: %s, got %s", expected, w.hostname) 57 | } 58 | <-done 59 | } 60 | 61 | func TestWriteFormatters(t *testing.T) { 62 | tests := []struct { 63 | name string 64 | f Formatter 65 | }{ 66 | {"default", nil}, 67 | {"unix", UnixFormatter}, 68 | {"rfc 3164", RFC3164Formatter}, 69 | {"rfc 5424", RFC5424Formatter}, 70 | {"default", DefaultFormatter}, 71 | } 72 | 73 | for _, test := range tests { 74 | done := make(chan string) 75 | addr, sock, srvWG := startServer("udp", "", done) 76 | defer sock.Close() 77 | defer srvWG.Wait() 78 | 79 | w := Writer{ 80 | priority: LOG_ERR, 81 | tag: "tag", 82 | hostname: "hostname", 83 | network: "udp", 84 | raddr: addr, 85 | } 86 | 87 | _, err := w.connect() 88 | if err != nil { 89 | t.Errorf("failed to connect: %v", err) 90 | } 91 | defer w.Close() 92 | 93 | w.SetFormatter(test.f) 94 | 95 | f := test.f 96 | if f == nil { 97 | f = DefaultFormatter 98 | } 99 | expected := strings.TrimSpace(f(LOG_ERR, "hostname", "tag", "this is a test message")) 100 | 101 | _, err = w.Write([]byte("this is a test message")) 102 | if err != nil { 103 | t.Errorf("failed to write: %v", err) 104 | } 105 | sent := strings.TrimSpace(<-done) 106 | if sent != expected { 107 | t.Errorf("expected to use the %v formatter, got %v, expected %v", test.name, sent, expected) 108 | } 109 | } 110 | } 111 | 112 | func TestWriterFramers(t *testing.T) { 113 | tests := []struct { 114 | name string 115 | f Framer 116 | }{ 117 | {"default", nil}, 118 | {"rfc 5425", RFC5425MessageLengthFramer}, 119 | {"default", DefaultFramer}, 120 | } 121 | 122 | for _, test := range tests { 123 | done := make(chan string) 124 | addr, sock, srvWG := startServer("udp", "", done) 125 | defer sock.Close() 126 | defer srvWG.Wait() 127 | 128 | w := Writer{ 129 | priority: LOG_ERR, 130 | tag: "tag", 131 | hostname: "hostname", 132 | network: "udp", 133 | raddr: addr, 134 | } 135 | 136 | _, err := w.connect() 137 | if err != nil { 138 | t.Errorf("failed to connect: %v", err) 139 | } 140 | defer w.Close() 141 | 142 | w.SetFramer(test.f) 143 | 144 | f := test.f 145 | if f == nil { 146 | f = DefaultFramer 147 | } 148 | expected := strings.TrimSpace(f(DefaultFormatter(LOG_ERR, "hostname", "tag", "this is a test message") + "\n")) 149 | 150 | _, err = w.Write([]byte("this is a test message")) 151 | if err != nil { 152 | t.Errorf("failed to write: %v", err) 153 | } 154 | sent := strings.TrimSpace(<-done) 155 | if sent != expected { 156 | t.Errorf("expected to use the %v framer, got %v, expected %v", test.name, sent, expected) 157 | } 158 | } 159 | } 160 | 161 | func TestWriteWithDefaultPriority(t *testing.T) { 162 | done := make(chan string) 163 | addr, sock, srvWG := startServer("udp", "", done) 164 | defer sock.Close() 165 | defer srvWG.Wait() 166 | 167 | w := Writer{ 168 | priority: LOG_ERR, 169 | tag: "tag", 170 | hostname: "hostname", 171 | network: "udp", 172 | raddr: addr, 173 | } 174 | 175 | _, err := w.connect() 176 | if err != nil { 177 | t.Errorf("failed to connect: %v", err) 178 | } 179 | defer w.Close() 180 | 181 | var bytes int 182 | bytes, err = w.Write([]byte("this is a test message")) 183 | if err != nil { 184 | t.Errorf("failed to write: %v", err) 185 | } 186 | if bytes == 0 { 187 | t.Errorf("zero bytes written") 188 | } 189 | 190 | checkWithPriorityAndTag(t, LOG_ERR, "tag", "hostname", "this is a test message", <-done) 191 | } 192 | 193 | func TestWriteWithPriority(t *testing.T) { 194 | done := make(chan string) 195 | addr, sock, srvWG := startServer("udp", "", done) 196 | defer sock.Close() 197 | defer srvWG.Wait() 198 | 199 | w := Writer{ 200 | priority: LOG_ERR, 201 | tag: "tag", 202 | hostname: "hostname", 203 | network: "udp", 204 | raddr: addr, 205 | } 206 | 207 | _, err := w.connect() 208 | if err != nil { 209 | t.Errorf("failed to connect: %v", err) 210 | } 211 | defer w.Close() 212 | 213 | var bytes int 214 | bytes, err = w.WriteWithPriority(LOG_DEBUG, []byte("this is a test message")) 215 | if err != nil { 216 | t.Errorf("failed to write: %v", err) 217 | } 218 | if bytes == 0 { 219 | t.Errorf("zero bytes written") 220 | } 221 | 222 | checkWithPriorityAndTag(t, LOG_DEBUG, "tag", "hostname", "this is a test message", <-done) 223 | } 224 | 225 | func TestWriteWithPriorityAndFacility(t *testing.T) { 226 | done := make(chan string) 227 | addr, sock, srvWG := startServer("udp", "", done) 228 | defer sock.Close() 229 | defer srvWG.Wait() 230 | 231 | w := Writer{ 232 | priority: LOG_ERR, 233 | tag: "tag", 234 | hostname: "hostname", 235 | network: "udp", 236 | raddr: addr, 237 | } 238 | 239 | _, err := w.connect() 240 | if err != nil { 241 | t.Errorf("failed to connect: %v", err) 242 | } 243 | defer w.Close() 244 | 245 | var bytes int 246 | bytes, err = w.WriteWithPriority(LOG_DEBUG|LOG_LOCAL5, []byte("this is a test message")) 247 | if err != nil { 248 | t.Errorf("failed to write: %v", err) 249 | } 250 | if bytes == 0 { 251 | t.Errorf("zero bytes written") 252 | } 253 | 254 | checkWithPriorityAndTag(t, LOG_DEBUG|LOG_LOCAL5, "tag", "hostname", "this is a test message", <-done) 255 | } 256 | 257 | func TestDebug(t *testing.T) { 258 | done := make(chan string) 259 | addr, sock, srvWG := startServer("udp", "", done) 260 | defer sock.Close() 261 | defer srvWG.Wait() 262 | 263 | w := Writer{ 264 | priority: LOG_ERR, 265 | tag: "tag", 266 | hostname: "hostname", 267 | network: "udp", 268 | raddr: addr, 269 | } 270 | 271 | _, err := w.connect() 272 | if err != nil { 273 | t.Errorf("failed to connect: %v", err) 274 | } 275 | defer w.Close() 276 | 277 | err = w.Debug("this is a test message") 278 | if err != nil { 279 | t.Errorf("failed to debug: %v", err) 280 | } 281 | 282 | checkWithPriorityAndTag(t, LOG_DEBUG, "tag", "hostname", "this is a test message", <-done) 283 | } 284 | 285 | func TestInfo(t *testing.T) { 286 | done := make(chan string) 287 | addr, sock, srvWG := startServer("udp", "", done) 288 | defer sock.Close() 289 | defer srvWG.Wait() 290 | 291 | w := Writer{ 292 | priority: LOG_ERR, 293 | tag: "tag", 294 | hostname: "hostname", 295 | network: "udp", 296 | raddr: addr, 297 | } 298 | 299 | _, err := w.connect() 300 | if err != nil { 301 | t.Errorf("failed to connect: %v", err) 302 | } 303 | defer w.Close() 304 | 305 | err = w.Info("this is a test message") 306 | if err != nil { 307 | t.Errorf("failed to info: %v", err) 308 | } 309 | 310 | checkWithPriorityAndTag(t, LOG_INFO, "tag", "hostname", "this is a test message", <-done) 311 | } 312 | 313 | func TestNotice(t *testing.T) { 314 | done := make(chan string) 315 | addr, sock, srvWG := startServer("udp", "", done) 316 | defer sock.Close() 317 | defer srvWG.Wait() 318 | 319 | w := Writer{ 320 | priority: LOG_ERR, 321 | tag: "tag", 322 | hostname: "hostname", 323 | network: "udp", 324 | raddr: addr, 325 | } 326 | 327 | _, err := w.connect() 328 | if err != nil { 329 | t.Errorf("failed to connect: %v", err) 330 | } 331 | defer w.Close() 332 | 333 | err = w.Notice("this is a test message") 334 | if err != nil { 335 | t.Errorf("failed to notice: %v", err) 336 | } 337 | 338 | checkWithPriorityAndTag(t, LOG_NOTICE, "tag", "hostname", "this is a test message", <-done) 339 | } 340 | 341 | func TestWarning(t *testing.T) { 342 | done := make(chan string) 343 | addr, sock, srvWG := startServer("udp", "", done) 344 | defer sock.Close() 345 | defer srvWG.Wait() 346 | 347 | w := Writer{ 348 | priority: LOG_ERR, 349 | tag: "tag", 350 | hostname: "hostname", 351 | network: "udp", 352 | raddr: addr, 353 | } 354 | 355 | _, err := w.connect() 356 | if err != nil { 357 | t.Errorf("failed to connect: %v", err) 358 | } 359 | defer w.Close() 360 | 361 | err = w.Warning("this is a test message") 362 | if err != nil { 363 | t.Errorf("failed to warn: %v", err) 364 | } 365 | 366 | checkWithPriorityAndTag(t, LOG_WARNING, "tag", "hostname", "this is a test message", <-done) 367 | } 368 | 369 | func TestErr(t *testing.T) { 370 | done := make(chan string) 371 | addr, sock, srvWG := startServer("udp", "", done) 372 | defer sock.Close() 373 | defer srvWG.Wait() 374 | 375 | w := Writer{ 376 | priority: LOG_ERR, 377 | tag: "tag", 378 | hostname: "hostname", 379 | network: "udp", 380 | raddr: addr, 381 | } 382 | 383 | _, err := w.connect() 384 | if err != nil { 385 | t.Errorf("failed to connect: %v", err) 386 | } 387 | defer w.Close() 388 | 389 | err = w.Err("this is a test message") 390 | if err != nil { 391 | t.Errorf("failed to err: %v", err) 392 | } 393 | 394 | checkWithPriorityAndTag(t, LOG_ERR, "tag", "hostname", "this is a test message", <-done) 395 | } 396 | 397 | func TestCrit(t *testing.T) { 398 | done := make(chan string) 399 | addr, sock, srvWG := startServer("udp", "", done) 400 | defer sock.Close() 401 | defer srvWG.Wait() 402 | 403 | w := Writer{ 404 | priority: LOG_ERR, 405 | tag: "tag", 406 | hostname: "hostname", 407 | network: "udp", 408 | raddr: addr, 409 | } 410 | 411 | _, err := w.connect() 412 | if err != nil { 413 | t.Errorf("failed to connect: %v", err) 414 | } 415 | defer w.Close() 416 | 417 | err = w.Crit("this is a test message") 418 | if err != nil { 419 | t.Errorf("failed to crit: %v", err) 420 | } 421 | 422 | checkWithPriorityAndTag(t, LOG_CRIT, "tag", "hostname", "this is a test message", <-done) 423 | } 424 | 425 | func TestAlert(t *testing.T) { 426 | done := make(chan string) 427 | addr, sock, srvWG := startServer("udp", "", done) 428 | defer sock.Close() 429 | defer srvWG.Wait() 430 | 431 | w := Writer{ 432 | priority: LOG_ERR, 433 | tag: "tag", 434 | hostname: "hostname", 435 | network: "udp", 436 | raddr: addr, 437 | } 438 | 439 | _, err := w.connect() 440 | if err != nil { 441 | t.Errorf("failed to connect: %v", err) 442 | } 443 | defer w.Close() 444 | 445 | err = w.Alert("this is a test message") 446 | if err != nil { 447 | t.Errorf("failed to alert: %v", err) 448 | } 449 | 450 | checkWithPriorityAndTag(t, LOG_ALERT, "tag", "hostname", "this is a test message", <-done) 451 | } 452 | 453 | func TestEmerg(t *testing.T) { 454 | done := make(chan string) 455 | addr, sock, srvWG := startServer("udp", "", done) 456 | defer sock.Close() 457 | defer srvWG.Wait() 458 | 459 | w := Writer{ 460 | priority: LOG_ERR, 461 | tag: "tag", 462 | hostname: "hostname", 463 | network: "udp", 464 | raddr: addr, 465 | } 466 | 467 | _, err := w.connect() 468 | if err != nil { 469 | t.Errorf("failed to connect: %v", err) 470 | } 471 | defer w.Close() 472 | 473 | err = w.Emerg("this is a test message") 474 | if err != nil { 475 | t.Errorf("failed to emerg: %v", err) 476 | } 477 | 478 | checkWithPriorityAndTag(t, LOG_EMERG, "tag", "hostname", "this is a test message", <-done) 479 | } 480 | -------------------------------------------------------------------------------- /srslog_test.go: -------------------------------------------------------------------------------- 1 | package srslog 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "os" 12 | "runtime" 13 | "sync" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func runPktSyslog(c net.PacketConn, done chan<- string) { 19 | var buf [4096]byte 20 | var rcvd string 21 | ct := 0 22 | for { 23 | var n int 24 | var err error 25 | 26 | c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 27 | n, _, err = c.ReadFrom(buf[:]) 28 | rcvd += string(buf[:n]) 29 | if err != nil { 30 | if oe, ok := err.(*net.OpError); ok { 31 | if ct < 3 && oe.Temporary() { 32 | ct++ 33 | continue 34 | } 35 | } 36 | break 37 | } 38 | } 39 | c.Close() 40 | done <- rcvd 41 | } 42 | 43 | type Crashy struct { 44 | sync.RWMutex 45 | is bool 46 | } 47 | 48 | func (c *Crashy) IsCrashy() bool { 49 | c.RLock() 50 | defer c.RUnlock() 51 | return c.is 52 | } 53 | 54 | func (c *Crashy) Set(is bool) { 55 | c.Lock() 56 | c.is = is 57 | c.Unlock() 58 | } 59 | 60 | var crashy = Crashy{is: false} 61 | 62 | func testableNetwork(network string) bool { 63 | switch network { 64 | case "unix", "unixgram": 65 | switch runtime.GOOS { 66 | case "darwin": 67 | switch runtime.GOARCH { 68 | case "arm", "arm64": 69 | return false 70 | } 71 | case "android": 72 | return false 73 | } 74 | } 75 | return true 76 | } 77 | 78 | func runStreamSyslog(l net.Listener, done chan<- string, wg *sync.WaitGroup) { 79 | for { 80 | var c net.Conn 81 | var err error 82 | if c, err = l.Accept(); err != nil { 83 | return 84 | } 85 | wg.Add(1) 86 | go func(c net.Conn) { 87 | defer wg.Done() 88 | c.SetReadDeadline(time.Now().Add(5 * time.Second)) 89 | b := bufio.NewReader(c) 90 | for ct := 1; !crashy.IsCrashy() || ct&7 != 0; ct++ { 91 | s, err := b.ReadString('\n') 92 | if err != nil { 93 | break 94 | } 95 | done <- s 96 | } 97 | c.Close() 98 | }(c) 99 | } 100 | } 101 | 102 | func startServer(n, la string, done chan<- string) (addr string, sock io.Closer, wg *sync.WaitGroup) { 103 | if n == "udp" || n == "tcp" || n == "tcp+tls" { 104 | la = "127.0.0.1:0" 105 | } else { 106 | // unix and unixgram: choose an address if none given 107 | if la == "" { 108 | // use ioutil.TempFile to get a name that is unique 109 | f, err := ioutil.TempFile("", "syslogtest") 110 | if err != nil { 111 | log.Fatal("TempFile: ", err) 112 | } 113 | f.Close() 114 | la = f.Name() 115 | } 116 | os.Remove(la) 117 | } 118 | 119 | wg = new(sync.WaitGroup) 120 | if n == "udp" || n == "unixgram" { 121 | l, e := net.ListenPacket(n, la) 122 | if e != nil { 123 | log.Fatalf("startServer failed: %v", e) 124 | } 125 | addr = l.LocalAddr().String() 126 | sock = l 127 | wg.Add(1) 128 | go func() { 129 | defer wg.Done() 130 | runPktSyslog(l, done) 131 | }() 132 | } else if n == "tcp+tls" { 133 | cert, err := tls.LoadX509KeyPair("test/cert.pem", "test/privkey.pem") 134 | if err != nil { 135 | log.Fatalf("failed to load TLS keypair: %v", err) 136 | } 137 | config := tls.Config{Certificates: []tls.Certificate{cert}} 138 | l, e := tls.Listen("tcp", la, &config) 139 | if e != nil { 140 | log.Fatalf("startServer failed: %v", e) 141 | } 142 | addr = l.Addr().String() 143 | sock = l 144 | wg.Add(1) 145 | go func() { 146 | defer wg.Done() 147 | runStreamSyslog(l, done, wg) 148 | }() 149 | } else { 150 | l, e := net.Listen(n, la) 151 | if e != nil { 152 | log.Fatalf("startServer failed: %v", e) 153 | } 154 | addr = l.Addr().String() 155 | sock = l 156 | wg.Add(1) 157 | go func() { 158 | defer wg.Done() 159 | runStreamSyslog(l, done, wg) 160 | }() 161 | } 162 | return 163 | } 164 | 165 | func TestWithSimulated(t *testing.T) { 166 | msg := "Test 123" 167 | var transport []string 168 | for _, n := range []string{"unix", "unixgram", "udp", "tcp"} { 169 | if testableNetwork(n) { 170 | transport = append(transport, n) 171 | } 172 | } 173 | 174 | for _, tr := range transport { 175 | done := make(chan string) 176 | addr, sock, srvWG := startServer(tr, "", done) 177 | defer srvWG.Wait() 178 | defer sock.Close() 179 | if tr == "unix" || tr == "unixgram" { 180 | defer os.Remove(addr) 181 | } 182 | s, err := Dial(tr, addr, LOG_INFO|LOG_USER, "syslog_test") 183 | if err != nil { 184 | t.Fatalf("Dial() failed: %v", err) 185 | } 186 | err = s.Info(msg) 187 | if err != nil { 188 | t.Fatalf("log failed: %v", err) 189 | } 190 | check(t, msg, <-done) 191 | s.Close() 192 | } 193 | } 194 | 195 | func TestFlap(t *testing.T) { 196 | net := "unix" 197 | if !testableNetwork(net) { 198 | t.Skipf("skipping on %s/%s; 'unix' is not supported", runtime.GOOS, runtime.GOARCH) 199 | } 200 | 201 | done := make(chan string) 202 | addr, sock, srvWG := startServer(net, "", done) 203 | defer srvWG.Wait() 204 | defer os.Remove(addr) 205 | defer sock.Close() 206 | 207 | s, err := Dial(net, addr, LOG_INFO|LOG_USER, "syslog_test") 208 | if err != nil { 209 | t.Fatalf("Dial() failed: %v", err) 210 | } 211 | msg := "Moo 2" 212 | err = s.Info(msg) 213 | if err != nil { 214 | t.Fatalf("log failed: %v", err) 215 | } 216 | check(t, msg, <-done) 217 | 218 | // restart the server 219 | _, sock2, srvWG2 := startServer(net, addr, done) 220 | defer srvWG2.Wait() 221 | defer sock2.Close() 222 | 223 | // and try retransmitting 224 | msg = "Moo 3" 225 | err = s.Info(msg) 226 | if err != nil { 227 | t.Fatalf("log failed: %v", err) 228 | } 229 | check(t, msg, <-done) 230 | 231 | s.Close() 232 | } 233 | 234 | func TestNew(t *testing.T) { 235 | if LOG_LOCAL7 != 23<<3 { 236 | t.Fatalf("LOG_LOCAL7 has wrong value") 237 | } 238 | if testing.Short() { 239 | // Depends on syslog daemon running, and sometimes it's not. 240 | t.Skip("skipping syslog test during -short") 241 | } 242 | 243 | s, err := New(LOG_INFO|LOG_USER, "the_tag") 244 | if err != nil { 245 | t.Fatalf("New() failed: %s", err) 246 | } 247 | // Don't send any messages. 248 | s.Close() 249 | } 250 | 251 | func TestNewLogger(t *testing.T) { 252 | if testing.Short() { 253 | t.Skip("skipping syslog test during -short") 254 | } 255 | f, err := NewLogger(LOG_USER|LOG_INFO, 0) 256 | if f == nil { 257 | t.Error(err) 258 | } 259 | } 260 | 261 | func TestDial(t *testing.T) { 262 | if testing.Short() { 263 | t.Skip("skipping syslog test during -short") 264 | } 265 | f, err := Dial("", "", (LOG_LOCAL7|LOG_DEBUG)+1, "syslog_test") 266 | if f != nil { 267 | t.Fatalf("Should have trapped bad priority") 268 | } 269 | f, err = Dial("", "", -1, "syslog_test") 270 | if f != nil { 271 | t.Fatalf("Should have trapped bad priority") 272 | } 273 | l, err := Dial("", "", LOG_USER|LOG_ERR, "syslog_test") 274 | if err != nil { 275 | t.Fatalf("Dial() failed: %s", err) 276 | } 277 | l.Close() 278 | } 279 | 280 | func TestDialFails(t *testing.T) { 281 | w, err := Dial("udp", "fakehost", LOG_ERR, "tag") 282 | if err == nil { 283 | t.Errorf("should fail to dial") 284 | } 285 | if w != nil { 286 | t.Errorf("should not get a writer") 287 | } 288 | } 289 | 290 | func TestDialTLSFails(t *testing.T) { 291 | w, err := DialWithTLSCertPath("tcp+tls", "127.0.0.1:0", LOG_ERR, "syslog_test", "test/nocertfound.pem") 292 | if w != nil { 293 | t.Fatalf("Should not have a writer") 294 | } 295 | if err == nil { 296 | t.Fatalf("Should have failed to load the cert") 297 | } 298 | } 299 | 300 | func check(t *testing.T, in, out string) { 301 | if hostname, err := os.Hostname(); err != nil { 302 | t.Error("Error retrieving hostname") 303 | } else { 304 | checkWithPriorityAndTag(t, LOG_USER+LOG_INFO, "syslog_test", hostname, in, out) 305 | } 306 | } 307 | 308 | func checkWithPriorityAndTag(t *testing.T, p Priority, tag, hostname, in, out string) { 309 | tmpl := fmt.Sprintf("<%d>%%s %%s %s[%%d]: %s\n", p, tag, in) 310 | var parsedHostname, timestamp string 311 | var pid int 312 | if n, err := fmt.Sscanf(out, tmpl, ×tamp, &parsedHostname, &pid); n != 3 || err != nil { 313 | t.Errorf("Got %q, does not match template %q (%d %s)", out, tmpl, n, err) 314 | } else if hostname != parsedHostname { 315 | t.Errorf("hostname expected %v, got %v", hostname, parsedHostname) 316 | } 317 | } 318 | 319 | func TestWrite(t *testing.T) { 320 | tests := []struct { 321 | pri Priority 322 | pre string 323 | msg string 324 | exp string 325 | }{ 326 | {LOG_USER | LOG_ERR, "syslog_test", "", "%s %s syslog_test[%d]: \n"}, 327 | {LOG_USER | LOG_ERR, "syslog_test", "write test", "%s %s syslog_test[%d]: write test\n"}, 328 | // Write should not add \n if there already is one 329 | {LOG_USER | LOG_ERR, "syslog_test", "write test 2\n", "%s %s syslog_test[%d]: write test 2\n"}, 330 | } 331 | 332 | if hostname, err := os.Hostname(); err != nil { 333 | t.Fatalf("Error retrieving hostname") 334 | } else { 335 | for _, test := range tests { 336 | done := make(chan string) 337 | addr, sock, srvWG := startServer("udp", "", done) 338 | defer srvWG.Wait() 339 | defer sock.Close() 340 | l, err := Dial("udp", addr, test.pri, test.pre) 341 | if err != nil { 342 | t.Fatalf("syslog.Dial() failed: %v", err) 343 | } 344 | defer l.Close() 345 | _, err = io.WriteString(l, test.msg) 346 | if err != nil { 347 | t.Fatalf("WriteString() failed: %v", err) 348 | } 349 | rcvd := <-done 350 | test.exp = fmt.Sprintf("<%d>", test.pri) + test.exp 351 | var parsedHostname, timestamp string 352 | var pid int 353 | if n, err := fmt.Sscanf(rcvd, test.exp, ×tamp, &parsedHostname, &pid); n != 3 || err != nil || hostname != parsedHostname { 354 | t.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, test.exp, n, err) 355 | } 356 | } 357 | } 358 | } 359 | 360 | func TestTLSPathWrite(t *testing.T) { 361 | tests := []struct { 362 | pri Priority 363 | pre string 364 | msg string 365 | exp string 366 | }{ 367 | {LOG_USER | LOG_ERR, "syslog_test", "", "%s %s syslog_test[%d]: \n"}, 368 | {LOG_USER | LOG_ERR, "syslog_test", "write test", "%s %s syslog_test[%d]: write test\n"}, 369 | // Write should not add \n if there already is one 370 | {LOG_USER | LOG_ERR, "syslog_test", "write test 2\n", "%s %s syslog_test[%d]: write test 2\n"}, 371 | } 372 | 373 | if hostname, err := os.Hostname(); err != nil { 374 | t.Fatalf("Error retrieving hostname") 375 | } else { 376 | for _, test := range tests { 377 | done := make(chan string) 378 | addr, sock, srvWG := startServer("tcp+tls", "", done) 379 | defer srvWG.Wait() 380 | defer sock.Close() 381 | 382 | l, err := DialWithTLSCertPath("tcp+tls", addr, test.pri, test.pre, "test/cert.pem") 383 | if err != nil { 384 | t.Fatalf("syslog.Dial() failed: %v", err) 385 | } 386 | defer l.Close() 387 | _, err = io.WriteString(l, test.msg) 388 | if err != nil { 389 | t.Fatalf("WriteString() failed: %v", err) 390 | } 391 | rcvd := <-done 392 | test.exp = fmt.Sprintf("<%d>", test.pri) + test.exp 393 | var parsedHostname, timestamp string 394 | var pid int 395 | if n, err := fmt.Sscanf(rcvd, test.exp, ×tamp, &parsedHostname, &pid); n != 3 || err != nil || hostname != parsedHostname { 396 | t.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, test.exp, n, err) 397 | } 398 | } 399 | } 400 | } 401 | 402 | func TestTLSCertWrite(t *testing.T) { 403 | tests := []struct { 404 | pri Priority 405 | pre string 406 | msg string 407 | exp string 408 | }{ 409 | {LOG_USER | LOG_ERR, "syslog_test", "", "%s %s syslog_test[%d]: \n"}, 410 | {LOG_USER | LOG_ERR, "syslog_test", "write test", "%s %s syslog_test[%d]: write test\n"}, 411 | // Write should not add \n if there already is one 412 | {LOG_USER | LOG_ERR, "syslog_test", "write test 2\n", "%s %s syslog_test[%d]: write test 2\n"}, 413 | } 414 | 415 | if hostname, err := os.Hostname(); err != nil { 416 | t.Fatalf("Error retrieving hostname") 417 | } else { 418 | for _, test := range tests { 419 | done := make(chan string) 420 | addr, sock, srvWG := startServer("tcp+tls", "", done) 421 | defer srvWG.Wait() 422 | defer sock.Close() 423 | 424 | cert, err := ioutil.ReadFile("test/cert.pem") 425 | if err != nil { 426 | t.Fatalf("cold not read cert: %v", err) 427 | } 428 | 429 | l, err := DialWithTLSCert("tcp+tls", addr, test.pri, test.pre, cert) 430 | if err != nil { 431 | t.Fatalf("syslog.Dial() failed: %v", err) 432 | } 433 | defer l.Close() 434 | _, err = io.WriteString(l, test.msg) 435 | if err != nil { 436 | t.Fatalf("WriteString() failed: %v", err) 437 | } 438 | rcvd := <-done 439 | test.exp = fmt.Sprintf("<%d>", test.pri) + test.exp 440 | var parsedHostname, timestamp string 441 | var pid int 442 | if n, err := fmt.Sscanf(rcvd, test.exp, ×tamp, &parsedHostname, &pid); n != 3 || err != nil || hostname != parsedHostname { 443 | t.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, test.exp, n, err) 444 | } 445 | } 446 | } 447 | } 448 | 449 | func TestConcurrentWrite(t *testing.T) { 450 | addr, sock, srvWG := startServer("udp", "", make(chan string, 1)) 451 | defer srvWG.Wait() 452 | defer sock.Close() 453 | w, err := Dial("udp", addr, LOG_USER|LOG_ERR, "how's it going?") 454 | if err != nil { 455 | t.Fatalf("syslog.Dial() failed: %v", err) 456 | } 457 | var wg sync.WaitGroup 458 | for i := 0; i < 10; i++ { 459 | wg.Add(1) 460 | go func() { 461 | defer wg.Done() 462 | err := w.Info("test") 463 | if err != nil { 464 | t.Errorf("Info() failed: %v", err) 465 | return 466 | } 467 | }() 468 | } 469 | wg.Wait() 470 | } 471 | 472 | func TestConcurrentReconnect(t *testing.T) { 473 | crashy.Set(true) 474 | defer func() { crashy.Set(false) }() 475 | 476 | const N = 10 477 | const M = 100 478 | net := "unix" 479 | if !testableNetwork(net) { 480 | net = "tcp" 481 | if !testableNetwork(net) { 482 | t.Skipf("skipping on %s/%s; neither 'unix' or 'tcp' is supported", runtime.GOOS, runtime.GOARCH) 483 | } 484 | } 485 | done := make(chan string, N*M) 486 | addr, sock, srvWG := startServer(net, "", done) 487 | if net == "unix" { 488 | defer os.Remove(addr) 489 | } 490 | 491 | // count all the messages arriving 492 | count := make(chan int) 493 | go func() { 494 | ct := 0 495 | for range done { 496 | ct++ 497 | // we are looking for 500 out of 1000 events 498 | // here because lots of log messages are lost 499 | // in buffers (kernel and/or bufio) 500 | if ct > N*M/2 { 501 | break 502 | } 503 | } 504 | count <- ct 505 | }() 506 | 507 | var wg sync.WaitGroup 508 | wg.Add(N) 509 | for i := 0; i < N; i++ { 510 | go func() { 511 | defer wg.Done() 512 | w, err := Dial(net, addr, LOG_USER|LOG_ERR, "tag") 513 | if err != nil { 514 | t.Fatalf("syslog.Dial() failed: %v", err) 515 | } 516 | defer w.Close() 517 | for i := 0; i < M; i++ { 518 | err := w.Info("test") 519 | if err != nil { 520 | t.Errorf("Info() failed: %v", err) 521 | return 522 | } 523 | } 524 | }() 525 | } 526 | wg.Wait() 527 | sock.Close() 528 | srvWG.Wait() 529 | close(done) 530 | 531 | select { 532 | case <-count: 533 | case <-time.After(100 * time.Millisecond): 534 | t.Error("timeout in concurrent reconnect") 535 | } 536 | } 537 | 538 | func TestLocalConn(t *testing.T) { 539 | messages := make([]string, 0) 540 | conn := newTestLocalConn(&messages) 541 | 542 | lc := localConn{conn: conn} 543 | 544 | lc.writeString(nil, nil, LOG_ERR, "hostname", "tag", "content") 545 | 546 | if len(messages) != 1 { 547 | t.Errorf("should write one message") 548 | } 549 | 550 | if messages[0] != DefaultFramer(UnixFormatter(LOG_ERR, "hostname", "tag", "content")) { 551 | t.Errorf("should use the unix formatter") 552 | } 553 | } 554 | 555 | type testLocalConn struct { 556 | messages *[]string 557 | } 558 | 559 | func newTestLocalConn(messages *[]string) testLocalConn { 560 | return testLocalConn{ 561 | messages: messages, 562 | } 563 | } 564 | 565 | func (c testLocalConn) Write(b []byte) (int, error) { 566 | *c.messages = append(*c.messages, string(b)) 567 | return len(b), nil 568 | } 569 | 570 | func (c testLocalConn) Close() error { 571 | return nil 572 | } 573 | --------------------------------------------------------------------------------