blob: 603ad4c3b5389c3b5b1854c6e7a227b80a29bade [file] [log] [blame]
sslobodrd046be82019-01-16 10:02:22 -05001// 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 local systemd journal.
16// It is implemented in pure Go and connects to the journal directly over its
17// unix socket.
18//
19// To read from the journal, see the "sdjournal" package, which wraps the
20// sd-journal a C API.
21//
22// http://www.freedesktop.org/software/systemd/man/systemd-journald.service.html
23package journal
24
25import (
26 "bytes"
27 "encoding/binary"
28 "errors"
29 "fmt"
30 "io"
31 "io/ioutil"
32 "net"
33 "os"
34 "strconv"
35 "strings"
36 "syscall"
37)
38
39// Priority of a journal message
40type Priority int
41
42const (
43 PriEmerg Priority = iota
44 PriAlert
45 PriCrit
46 PriErr
47 PriWarning
48 PriNotice
49 PriInfo
50 PriDebug
51)
52
53var conn net.Conn
54
55func init() {
56 var err error
57 conn, err = net.Dial("unixgram", "/run/systemd/journal/socket")
58 if err != nil {
59 conn = nil
60 }
61}
62
63// Enabled returns true if the local systemd journal is available for logging
64func Enabled() bool {
65 return conn != nil
66}
67
68// Send a message to the local systemd journal. vars is a map of journald
69// fields to values. Fields must be composed of uppercase letters, numbers,
70// and underscores, but must not start with an underscore. Within these
71// restrictions, any arbitrary field name may be used. Some names have special
72// significance: see the journalctl documentation
73// (http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html)
74// for more details. vars may be nil.
75func Send(message string, priority Priority, vars map[string]string) error {
76 if conn == nil {
77 return journalError("could not connect to journald socket")
78 }
79
80 data := new(bytes.Buffer)
81 appendVariable(data, "PRIORITY", strconv.Itoa(int(priority)))
82 appendVariable(data, "MESSAGE", message)
83 for k, v := range vars {
84 appendVariable(data, k, v)
85 }
86
87 _, err := io.Copy(conn, data)
88 if err != nil && isSocketSpaceError(err) {
89 file, err := tempFd()
90 if err != nil {
91 return journalError(err.Error())
92 }
93 defer file.Close()
94 _, err = io.Copy(file, data)
95 if err != nil {
96 return journalError(err.Error())
97 }
98
99 rights := syscall.UnixRights(int(file.Fd()))
100
101 /* this connection should always be a UnixConn, but better safe than sorry */
102 unixConn, ok := conn.(*net.UnixConn)
103 if !ok {
104 return journalError("can't send file through non-Unix connection")
105 }
106 _, _, err = unixConn.WriteMsgUnix([]byte{}, rights, nil)
107 if err != nil {
108 return journalError(err.Error())
109 }
110 } else if err != nil {
111 return journalError(err.Error())
112 }
113 return nil
114}
115
116// Print prints a message to the local systemd journal using Send().
117func Print(priority Priority, format string, a ...interface{}) error {
118 return Send(fmt.Sprintf(format, a...), priority, nil)
119}
120
121func appendVariable(w io.Writer, name, value string) {
122 if err := validVarName(name); err != nil {
123 journalError(err.Error())
124 }
125 if strings.ContainsRune(value, '\n') {
126 /* When the value contains a newline, we write:
127 * - the variable name, followed by a newline
128 * - the size (in 64bit little endian format)
129 * - the data, followed by a newline
130 */
131 fmt.Fprintln(w, name)
132 binary.Write(w, binary.LittleEndian, uint64(len(value)))
133 fmt.Fprintln(w, value)
134 } else {
135 /* just write the variable and value all on one line */
136 fmt.Fprintf(w, "%s=%s\n", name, value)
137 }
138}
139
140// validVarName validates a variable name to make sure it journald will accept it.
141// The variable name must be in uppercase and consist only of characters,
142// numbers and underscores, and may not begin with an underscore. (from the docs)
143// https://www.freedesktop.org/software/systemd/man/sd_journal_print.html
144func validVarName(name string) error {
145 if name == "" {
146 return errors.New("Empty variable name")
147 } else if name[0] == '_' {
148 return errors.New("Variable name begins with an underscore")
149 }
150
151 for _, c := range name {
152 if !(('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '_') {
153 return errors.New("Variable name contains invalid characters")
154 }
155 }
156 return nil
157}
158
159func isSocketSpaceError(err error) bool {
160 opErr, ok := err.(*net.OpError)
161 if !ok {
162 return false
163 }
164
165 sysErr, ok := opErr.Err.(syscall.Errno)
166 if !ok {
167 return false
168 }
169
170 return sysErr == syscall.EMSGSIZE || sysErr == syscall.ENOBUFS
171}
172
173func tempFd() (*os.File, error) {
174 file, err := ioutil.TempFile("/dev/shm/", "journal.XXXXX")
175 if err != nil {
176 return nil, err
177 }
178 err = syscall.Unlink(file.Name())
179 if err != nil {
180 return nil, err
181 }
182 return file, nil
183}
184
185func journalError(s string) error {
186 s = "journal error: " + s
187 fmt.Fprintln(os.Stderr, s)
188 return errors.New(s)
189}