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