├── go.mod ├── CHANGELOG.md ├── .golangci.yml ├── .gitignore ├── .github ├── dependabot.yaml ├── pull_request_template.md ├── CODEOWNERS └── workflows │ └── build_test.yml ├── README.md ├── unsupported.go ├── syslog.go ├── LICENSE ├── unix.go └── builtin.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-syslog 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ### Improvements 4 | 5 | ### Changes 6 | 7 | ### Fixed 8 | 9 | ### Security -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright IBM Corp. 2014, 2025 2 | # SPDX-License-Identifier: MIT 3 | 4 | linters: 5 | enable: 6 | - errcheck 7 | - gosimple 8 | - staticcheck 9 | output_format: colored-line-number -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Copyright IBM Corp. 2014, 2025 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | commit-message: 12 | prefix: "[chore] : " 13 | 14 | - package-ecosystem: "gomod" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | commit-message: 19 | prefix: "[chore] : " -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Description 3 | 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-syslog 2 | ========= 3 | 4 | This repository provides a very simple `gsyslog` package. The point of this 5 | package is to allow safe importing of syslog without introducing cross-compilation 6 | issues. The stdlib `log/syslog` cannot be imported on Windows systems, and without 7 | conditional compilation this adds complications. 8 | 9 | Instead, `gsyslog` provides a very simple wrapper around `log/syslog` but returns 10 | a runtime error if attempting to initialize on a non Linux or OSX system. 11 | 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team -------------------------------------------------------------------------------- /unsupported.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014, 2025 2 | // SPDX-License-Identifier: MIT 3 | 4 | // +build windows plan9 nacl 5 | 6 | package gsyslog 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | // NewLogger is used to construct a new Syslogger 13 | func NewLogger(p Priority, facility, tag string) (Syslogger, error) { 14 | return nil, fmt.Errorf("Platform does not support syslog") 15 | } 16 | 17 | // DialLogger is used to construct a new Syslogger that establishes connection to remote syslog server 18 | func DialLogger(network, raddr string, p Priority, facility, tag string) (Syslogger, error) { 19 | return nil, fmt.Errorf("Platform does not support syslog") 20 | } 21 | -------------------------------------------------------------------------------- /syslog.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014, 2025 2 | // SPDX-License-Identifier: MIT 3 | 4 | package gsyslog 5 | 6 | // Priority maps to the syslog priority levels 7 | type Priority int 8 | 9 | const ( 10 | LOG_EMERG Priority = iota 11 | LOG_ALERT 12 | LOG_CRIT 13 | LOG_ERR 14 | LOG_WARNING 15 | LOG_NOTICE 16 | LOG_INFO 17 | LOG_DEBUG 18 | ) 19 | 20 | // Syslogger interface is used to write log messages to syslog 21 | type Syslogger interface { 22 | // WriteLevel is used to write a message at a given level 23 | WriteLevel(Priority, []byte) error 24 | 25 | // Write is used to write a message at the default level 26 | Write([]byte) (int, error) 27 | 28 | // Close is used to close the connection to the logger 29 | Close() error 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright IBM Corp. 2014, 2025 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /.github/workflows/build_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test Workflow 2 | 3 | on: 4 | pull_request: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 14 | - name: Setup Go 15 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 16 | with: 17 | go-version: '1.23' 18 | - name: Run golangci-lint 19 | uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 20 | - name: Run test and generate reports 21 | run: go test -v ./... -coverprofile=coverage.out 22 | - name: Upload Coverage Test 23 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 24 | with: 25 | name: Coverage-report 26 | path: coverage.out 27 | - name: Display the report 28 | run: go tool cover -func=coverage.out 29 | - name: Build Go 30 | run: go build ./... -------------------------------------------------------------------------------- /unix.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014, 2025 2 | // SPDX-License-Identifier: MIT 3 | 4 | // +build linux darwin dragonfly freebsd netbsd openbsd solaris 5 | 6 | package gsyslog 7 | 8 | import ( 9 | "fmt" 10 | "log/syslog" 11 | "strings" 12 | ) 13 | 14 | // builtinLogger wraps the Golang implementation of a 15 | // syslog.Writer to provide the Syslogger interface 16 | type builtinLogger struct { 17 | *builtinWriter 18 | } 19 | 20 | // NewLogger is used to construct a new Syslogger 21 | func NewLogger(p Priority, facility, tag string) (Syslogger, error) { 22 | fPriority, err := facilityPriority(facility) 23 | if err != nil { 24 | return nil, err 25 | } 26 | priority := syslog.Priority(p) | fPriority 27 | l, err := newBuiltin(priority, tag) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &builtinLogger{l}, nil 32 | } 33 | 34 | // DialLogger is used to construct a new Syslogger that establishes connection to remote syslog server 35 | func DialLogger(network, raddr string, p Priority, facility, tag string) (Syslogger, error) { 36 | fPriority, err := facilityPriority(facility) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | priority := syslog.Priority(p) | fPriority 42 | 43 | l, err := dialBuiltin(network, raddr, priority, tag) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &builtinLogger{l}, nil 49 | } 50 | 51 | // WriteLevel writes out a message at the given priority 52 | func (b *builtinLogger) WriteLevel(p Priority, buf []byte) error { 53 | var err error 54 | m := string(buf) 55 | switch p { 56 | case LOG_EMERG: 57 | _, err = b.writeAndRetry(syslog.LOG_EMERG, m) 58 | case LOG_ALERT: 59 | _, err = b.writeAndRetry(syslog.LOG_ALERT, m) 60 | case LOG_CRIT: 61 | _, err = b.writeAndRetry(syslog.LOG_CRIT, m) 62 | case LOG_ERR: 63 | _, err = b.writeAndRetry(syslog.LOG_ERR, m) 64 | case LOG_WARNING: 65 | _, err = b.writeAndRetry(syslog.LOG_WARNING, m) 66 | case LOG_NOTICE: 67 | _, err = b.writeAndRetry(syslog.LOG_NOTICE, m) 68 | case LOG_INFO: 69 | _, err = b.writeAndRetry(syslog.LOG_INFO, m) 70 | case LOG_DEBUG: 71 | _, err = b.writeAndRetry(syslog.LOG_DEBUG, m) 72 | default: 73 | err = fmt.Errorf("Unknown priority: %v", p) 74 | } 75 | return err 76 | } 77 | 78 | // facilityPriority converts a facility string into 79 | // an appropriate priority level or returns an error 80 | func facilityPriority(facility string) (syslog.Priority, error) { 81 | facility = strings.ToUpper(facility) 82 | switch facility { 83 | case "KERN": 84 | return syslog.LOG_KERN, nil 85 | case "USER": 86 | return syslog.LOG_USER, nil 87 | case "MAIL": 88 | return syslog.LOG_MAIL, nil 89 | case "DAEMON": 90 | return syslog.LOG_DAEMON, nil 91 | case "AUTH": 92 | return syslog.LOG_AUTH, nil 93 | case "SYSLOG": 94 | return syslog.LOG_SYSLOG, nil 95 | case "LPR": 96 | return syslog.LOG_LPR, nil 97 | case "NEWS": 98 | return syslog.LOG_NEWS, nil 99 | case "UUCP": 100 | return syslog.LOG_UUCP, nil 101 | case "CRON": 102 | return syslog.LOG_CRON, nil 103 | case "AUTHPRIV": 104 | return syslog.LOG_AUTHPRIV, nil 105 | case "FTP": 106 | return syslog.LOG_FTP, nil 107 | case "LOCAL0": 108 | return syslog.LOG_LOCAL0, nil 109 | case "LOCAL1": 110 | return syslog.LOG_LOCAL1, nil 111 | case "LOCAL2": 112 | return syslog.LOG_LOCAL2, nil 113 | case "LOCAL3": 114 | return syslog.LOG_LOCAL3, nil 115 | case "LOCAL4": 116 | return syslog.LOG_LOCAL4, nil 117 | case "LOCAL5": 118 | return syslog.LOG_LOCAL5, nil 119 | case "LOCAL6": 120 | return syslog.LOG_LOCAL6, nil 121 | case "LOCAL7": 122 | return syslog.LOG_LOCAL7, nil 123 | default: 124 | return 0, fmt.Errorf("invalid syslog facility: %s", facility) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /builtin.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014, 2025 2 | // SPDX-License-Identifier: MIT 3 | 4 | // This file is taken from the log/syslog in the standard lib. 5 | // However, there is a bug with overwhelming syslog that causes writes 6 | // to block indefinitely. This is fixed by adding a write deadline. 7 | // 8 | //go:build !windows && !nacl && !plan9 9 | // +build !windows,!nacl,!plan9 10 | 11 | package gsyslog 12 | 13 | import ( 14 | "errors" 15 | "fmt" 16 | "log/syslog" 17 | "net" 18 | "os" 19 | "strings" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | const severityMask = 0x07 25 | const facilityMask = 0xf8 26 | const localDeadline = 20 * time.Millisecond 27 | const remoteDeadline = 50 * time.Millisecond 28 | 29 | // A builtinWriter is a connection to a syslog server. 30 | type builtinWriter struct { 31 | priority syslog.Priority 32 | tag string 33 | hostname string 34 | network string 35 | raddr string 36 | 37 | mu sync.Mutex // guards conn 38 | conn serverConn 39 | } 40 | 41 | // This interface and the separate syslog_unix.go file exist for 42 | // Solaris support as implemented by gccgo. On Solaris you can not 43 | // simply open a TCP connection to the syslog daemon. The gccgo 44 | // sources have a syslog_solaris.go file that implements unixSyslog to 45 | // return a type that satisfies this interface and simply calls the C 46 | // library syslog function. 47 | type serverConn interface { 48 | writeString(p syslog.Priority, hostname, tag, s, nl string) error 49 | close() error 50 | } 51 | 52 | type netConn struct { 53 | local bool 54 | conn net.Conn 55 | } 56 | 57 | // New establishes a new connection to the system log daemon. Each 58 | // write to the returned writer sends a log message with the given 59 | // priority and prefix. 60 | func newBuiltin(priority syslog.Priority, tag string) (w *builtinWriter, err error) { 61 | return dialBuiltin("", "", priority, tag) 62 | } 63 | 64 | // Dial establishes a connection to a log daemon by connecting to 65 | // address raddr on the specified network. Each write to the returned 66 | // writer sends a log message with the given facility, severity and 67 | // tag. 68 | // If network is empty, Dial will connect to the local syslog server. 69 | func dialBuiltin(network, raddr string, priority syslog.Priority, tag string) (*builtinWriter, error) { 70 | if priority < 0 || priority > syslog.LOG_LOCAL7|syslog.LOG_DEBUG { 71 | return nil, errors.New("log/syslog: invalid priority") 72 | } 73 | 74 | if tag == "" { 75 | tag = os.Args[0] 76 | } 77 | hostname, _ := os.Hostname() 78 | 79 | w := &builtinWriter{ 80 | priority: priority, 81 | tag: tag, 82 | hostname: hostname, 83 | network: network, 84 | raddr: raddr, 85 | } 86 | 87 | w.mu.Lock() 88 | defer w.mu.Unlock() 89 | 90 | err := w.connect() 91 | if err != nil { 92 | return nil, err 93 | } 94 | return w, err 95 | } 96 | 97 | // connect makes a connection to the syslog server. 98 | // It must be called with w.mu held. 99 | func (w *builtinWriter) connect() (err error) { 100 | if w.conn != nil { 101 | // ignore err from close, it makes sense to continue anyway 102 | w.conn.close() 103 | w.conn = nil 104 | } 105 | 106 | if w.network == "" { 107 | w.conn, err = unixSyslog() 108 | if w.hostname == "" { 109 | w.hostname = "localhost" 110 | } 111 | } else { 112 | var c net.Conn 113 | c, err = net.DialTimeout(w.network, w.raddr, remoteDeadline) 114 | if err == nil { 115 | w.conn = &netConn{conn: c} 116 | if w.hostname == "" { 117 | w.hostname = c.LocalAddr().String() 118 | } 119 | } 120 | } 121 | return 122 | } 123 | 124 | // Write sends a log message to the syslog daemon. 125 | func (w *builtinWriter) Write(b []byte) (int, error) { 126 | return w.writeAndRetry(w.priority, string(b)) 127 | } 128 | 129 | // Close closes a connection to the syslog daemon. 130 | func (w *builtinWriter) Close() error { 131 | w.mu.Lock() 132 | defer w.mu.Unlock() 133 | 134 | if w.conn != nil { 135 | err := w.conn.close() 136 | w.conn = nil 137 | return err 138 | } 139 | return nil 140 | } 141 | 142 | func (w *builtinWriter) writeAndRetry(p syslog.Priority, s string) (int, error) { 143 | pr := (w.priority & facilityMask) | (p & severityMask) 144 | 145 | w.mu.Lock() 146 | defer w.mu.Unlock() 147 | 148 | if w.conn != nil { 149 | if n, err := w.write(pr, s); err == nil { 150 | return n, err 151 | } 152 | } 153 | if err := w.connect(); err != nil { 154 | return 0, err 155 | } 156 | return w.write(pr, s) 157 | } 158 | 159 | // write generates and writes a syslog formatted string. The 160 | // format is as follows: TIMESTAMP HOSTNAME TAG[PID]: MSG 161 | func (w *builtinWriter) write(p syslog.Priority, msg string) (int, error) { 162 | // ensure it ends in a \n 163 | nl := "" 164 | if !strings.HasSuffix(msg, "\n") { 165 | nl = "\n" 166 | } 167 | 168 | err := w.conn.writeString(p, w.hostname, w.tag, msg, nl) 169 | if err != nil { 170 | return 0, err 171 | } 172 | // Note: return the length of the input, not the number of 173 | // bytes printed by Fprintf, because this must behave like 174 | // an io.Writer. 175 | return len(msg), nil 176 | } 177 | 178 | func (n *netConn) writeString(p syslog.Priority, hostname, tag, msg, nl string) error { 179 | if n.local { 180 | // Compared to the network form below, the changes are: 181 | // 1. Use time.Stamp instead of time.RFC3339. 182 | // 2. Drop the hostname field from the Fprintf. 183 | timestamp := time.Now().Format(time.Stamp) 184 | if err := n.conn.SetWriteDeadline(time.Now().Add(localDeadline)); err != nil { 185 | return fmt.Errorf("failed to set write deadline: %w", err) 186 | } 187 | _, err := fmt.Fprintf(n.conn, "<%d>%s %s[%d]: %s%s", 188 | p, timestamp, 189 | tag, os.Getpid(), msg, nl) 190 | return err 191 | } 192 | timestamp := time.Now().Format(time.RFC3339) 193 | if err := n.conn.SetWriteDeadline(time.Now().Add(remoteDeadline)); err != nil { 194 | return fmt.Errorf("failed to set write deadline: %w", err) 195 | } 196 | _, err := fmt.Fprintf(n.conn, "<%d>%s %s %s[%d]: %s%s", 197 | p, timestamp, hostname, 198 | tag, os.Getpid(), msg, nl) 199 | return err 200 | } 201 | 202 | func (n *netConn) close() error { 203 | return n.conn.Close() 204 | } 205 | 206 | // unixSyslog opens a connection to the syslog daemon running on the 207 | // local machine using a Unix domain socket. 208 | func unixSyslog() (conn serverConn, err error) { 209 | logTypes := []string{"unixgram", "unix"} 210 | logPaths := []string{"/dev/log", "/var/run/syslog", "/var/run/log"} 211 | for _, network := range logTypes { 212 | for _, path := range logPaths { 213 | conn, err := net.DialTimeout(network, path, localDeadline) 214 | if err != nil { 215 | continue 216 | } else { 217 | return &netConn{conn: conn, local: true}, nil 218 | } 219 | } 220 | } 221 | return nil, errors.New("Unix syslog delivery error") 222 | } 223 | --------------------------------------------------------------------------------