1// Copyright 2015 CoreOS, Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package journal provides write bindings to the systemd journal 16package journal 17 18import ( 19 "bytes" 20 "encoding/binary" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net" 26 "os" 27 "strconv" 28 "strings" 29 "syscall" 30) 31 32// Priority of a journal message 33type Priority int 34 35const ( 36 PriEmerg Priority = iota 37 PriAlert 38 PriCrit 39 PriErr 40 PriWarning 41 PriNotice 42 PriInfo 43 PriDebug 44) 45 46var conn net.Conn 47 48func init() { 49 var err error 50 conn, err = net.Dial("unixgram", "/run/systemd/journal/socket") 51 if err != nil { 52 conn = nil 53 } 54} 55 56// Enabled returns true iff the systemd journal is available for logging 57func Enabled() bool { 58 return conn != nil 59} 60 61// Send a message to the systemd journal. vars is a map of journald fields to 62// values. Fields must be composed of uppercase letters, numbers, and 63// underscores, but must not start with an underscore. Within these 64// restrictions, any arbitrary field name may be used. Some names have special 65// significance: see the journalctl documentation 66// (http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html) 67// for more details. vars may be nil. 68func Send(message string, priority Priority, vars map[string]string) error { 69 if conn == nil { 70 return journalError("could not connect to journald socket") 71 } 72 73 data := new(bytes.Buffer) 74 appendVariable(data, "PRIORITY", strconv.Itoa(int(priority))) 75 appendVariable(data, "MESSAGE", message) 76 for k, v := range vars { 77 appendVariable(data, k, v) 78 } 79 80 _, err := io.Copy(conn, data) 81 if err != nil && isSocketSpaceError(err) { 82 file, err := tempFd() 83 if err != nil { 84 return journalError(err.Error()) 85 } 86 _, err = io.Copy(file, data) 87 if err != nil { 88 return journalError(err.Error()) 89 } 90 91 rights := syscall.UnixRights(int(file.Fd())) 92 93 /* this connection should always be a UnixConn, but better safe than sorry */ 94 unixConn, ok := conn.(*net.UnixConn) 95 if !ok { 96 return journalError("can't send file through non-Unix connection") 97 } 98 unixConn.WriteMsgUnix([]byte{}, rights, nil) 99 } else if err != nil { 100 return journalError(err.Error()) 101 } 102 return nil 103} 104 105func appendVariable(w io.Writer, name, value string) { 106 if !validVarName(name) { 107 journalError("variable name contains invalid character, ignoring") 108 } 109 if strings.ContainsRune(value, '\n') { 110 /* When the value contains a newline, we write: 111 * - the variable name, followed by a newline 112 * - the size (in 64bit little endian format) 113 * - the data, followed by a newline 114 */ 115 fmt.Fprintln(w, name) 116 binary.Write(w, binary.LittleEndian, uint64(len(value))) 117 fmt.Fprintln(w, value) 118 } else { 119 /* just write the variable and value all on one line */ 120 fmt.Fprintf(w, "%s=%s\n", name, value) 121 } 122} 123 124func validVarName(name string) bool { 125 /* The variable name must be in uppercase and consist only of characters, 126 * numbers and underscores, and may not begin with an underscore. (from the docs) 127 */ 128 129 valid := name[0] != '_' 130 for _, c := range name { 131 valid = valid && ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '_' 132 } 133 return valid 134} 135 136func isSocketSpaceError(err error) bool { 137 opErr, ok := err.(*net.OpError) 138 if !ok { 139 return false 140 } 141 142 sysErr, ok := opErr.Err.(syscall.Errno) 143 if !ok { 144 return false 145 } 146 147 return sysErr == syscall.EMSGSIZE || sysErr == syscall.ENOBUFS 148} 149 150func tempFd() (*os.File, error) { 151 file, err := ioutil.TempFile("/dev/shm/", "journal.XXXXX") 152 if err != nil { 153 return nil, err 154 } 155 syscall.Unlink(file.Name()) 156 if err != nil { 157 return nil, err 158 } 159 return file, nil 160} 161 162func journalError(s string) error { 163 s = "journal error: " + s 164 fmt.Fprintln(os.Stderr, s) 165 return errors.New(s) 166} 167