Skip to content

Commit ffb1b0d

Browse files
committed
logging: add journald encoder wrapper
1 parent 4f50458 commit ffb1b0d

File tree

3 files changed

+423
-0
lines changed

3 files changed

+423
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
log {
3+
format journald {
4+
wrap console
5+
}
6+
}
7+
}
8+
9+
:80 {
10+
respond "Hello, World!"
11+
}
12+
----------
13+
{
14+
"logging": {
15+
"logs": {
16+
"default": {
17+
"encoder": {
18+
"format": "journald",
19+
"wrap": {
20+
"format": "console"
21+
}
22+
}
23+
}
24+
}
25+
},
26+
"apps": {
27+
"http": {
28+
"servers": {
29+
"srv0": {
30+
"listen": [
31+
":80"
32+
],
33+
"routes": [
34+
{
35+
"handle": [
36+
{
37+
"body": "Hello, World!",
38+
"handler": "static_response"
39+
}
40+
]
41+
}
42+
]
43+
}
44+
}
45+
}
46+
}
47+
}

modules/logging/journaldencoder.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright 2015 Matthew Holt and The Caddy Authors
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 logging
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"os"
21+
22+
"go.uber.org/zap/buffer"
23+
"go.uber.org/zap/zapcore"
24+
"golang.org/x/term"
25+
26+
"github.com/caddyserver/caddy/v2"
27+
"github.com/caddyserver/caddy/v2/caddyconfig"
28+
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
29+
)
30+
31+
func init() {
32+
caddy.RegisterModule(JournaldEncoder{})
33+
}
34+
35+
// JournaldEncoder wraps another encoder and prepends a systemd/journald
36+
// priority prefix to each emitted log line. This lets journald classify
37+
// stdout/stderr log lines by severity while leaving the underlying log
38+
// structure to the wrapped encoder.
39+
//
40+
// This encoder does not write directly to journald; it only changes the
41+
// encoded output by adding the priority marker that journald understands.
42+
// The wrapped encoder still controls the actual log format, such as JSON
43+
// or console output.
44+
type JournaldEncoder struct {
45+
zapcore.Encoder `json:"-"`
46+
47+
// The underlying encoder that actually encodes the log entries.
48+
// If not specified, defaults to "json", unless the output is a
49+
// terminal, in which case it defaults to "console".
50+
WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
51+
52+
wrappedIsDefault bool
53+
ctx caddy.Context
54+
}
55+
56+
// CaddyModule returns the Caddy module information.
57+
func (JournaldEncoder) CaddyModule() caddy.ModuleInfo {
58+
return caddy.ModuleInfo{
59+
ID: "caddy.logging.encoders.journald",
60+
New: func() caddy.Module { return new(JournaldEncoder) },
61+
}
62+
}
63+
64+
// Provision sets up the encoder.
65+
func (je *JournaldEncoder) Provision(ctx caddy.Context) error {
66+
je.ctx = ctx
67+
68+
if je.WrappedRaw == nil {
69+
je.Encoder = &JSONEncoder{}
70+
if p, ok := je.Encoder.(caddy.Provisioner); ok {
71+
if err := p.Provision(ctx); err != nil {
72+
return fmt.Errorf("provisioning fallback encoder module: %v", err)
73+
}
74+
}
75+
je.wrappedIsDefault = true
76+
} else {
77+
val, err := ctx.LoadModule(je, "WrappedRaw")
78+
if err != nil {
79+
return fmt.Errorf("loading wrapped encoder module: %v", err)
80+
}
81+
je.Encoder = val.(zapcore.Encoder)
82+
}
83+
84+
suppressEncoderTimestamp(je.Encoder)
85+
86+
return nil
87+
}
88+
89+
// ConfigureDefaultFormat will set the default wrapped format to "console"
90+
// if the writer is a terminal. If already configured, it passes through
91+
// the writer so a deeply nested encoder can configure its own default format.
92+
func (je *JournaldEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error {
93+
if !je.wrappedIsDefault {
94+
if cfd, ok := je.Encoder.(caddy.ConfiguresFormatterDefault); ok {
95+
return cfd.ConfigureDefaultFormat(wo)
96+
}
97+
return nil
98+
}
99+
100+
if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) {
101+
je.Encoder = &ConsoleEncoder{}
102+
if p, ok := je.Encoder.(caddy.Provisioner); ok {
103+
if err := p.Provision(je.ctx); err != nil {
104+
return fmt.Errorf("provisioning fallback encoder module: %v", err)
105+
}
106+
}
107+
}
108+
109+
suppressEncoderTimestamp(je.Encoder)
110+
111+
return nil
112+
}
113+
114+
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
115+
//
116+
// journald {
117+
// wrap <another encoder>
118+
// }
119+
//
120+
// Example:
121+
//
122+
// log {
123+
// format journald {
124+
// wrap json
125+
// }
126+
// }
127+
func (je *JournaldEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
128+
d.Next() // consume encoder name
129+
if d.NextArg() {
130+
return d.ArgErr()
131+
}
132+
133+
for d.NextBlock(0) {
134+
if d.Val() != "wrap" {
135+
return d.Errf("unrecognized subdirective %s", d.Val())
136+
}
137+
if !d.NextArg() {
138+
return d.ArgErr()
139+
}
140+
moduleName := d.Val()
141+
moduleID := "caddy.logging.encoders." + moduleName
142+
unm, err := caddyfile.UnmarshalModule(d, moduleID)
143+
if err != nil {
144+
return err
145+
}
146+
enc, ok := unm.(zapcore.Encoder)
147+
if !ok {
148+
return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
149+
}
150+
je.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil)
151+
}
152+
153+
return nil
154+
}
155+
156+
// Clone implements zapcore.Encoder.
157+
func (je JournaldEncoder) Clone() zapcore.Encoder {
158+
return JournaldEncoder{
159+
Encoder: je.Encoder.Clone(),
160+
}
161+
}
162+
163+
// EncodeEntry implements zapcore.Encoder.
164+
func (je JournaldEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
165+
encoded, err := je.Encoder.Clone().EncodeEntry(ent, fields)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
out := bufferpool.Get()
171+
out.AppendString(journaldPriorityPrefix(ent.Level))
172+
out.AppendBytes(encoded.Bytes())
173+
encoded.Free()
174+
175+
return out, nil
176+
}
177+
178+
func journaldPriorityPrefix(level zapcore.Level) string {
179+
switch level {
180+
case zapcore.InvalidLevel:
181+
return "<6>"
182+
case zapcore.DebugLevel:
183+
return "<7>"
184+
case zapcore.InfoLevel:
185+
return "<6>"
186+
case zapcore.WarnLevel:
187+
return "<4>"
188+
case zapcore.ErrorLevel:
189+
return "<3>"
190+
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
191+
return "<2>"
192+
default:
193+
return "<6>"
194+
}
195+
}
196+
197+
func suppressEncoderTimestamp(enc zapcore.Encoder) {
198+
empty := ""
199+
200+
switch e := enc.(type) {
201+
case *ConsoleEncoder:
202+
e.TimeKey = &empty
203+
_ = e.Provision(caddy.Context{})
204+
case *JSONEncoder:
205+
e.TimeKey = &empty
206+
_ = e.Provision(caddy.Context{})
207+
case *AppendEncoder:
208+
suppressEncoderTimestamp(e.wrapped)
209+
case *FilterEncoder:
210+
suppressEncoderTimestamp(e.wrapped)
211+
case *JournaldEncoder:
212+
suppressEncoderTimestamp(e.Encoder)
213+
}
214+
}
215+
216+
// Interface guards
217+
var (
218+
_ zapcore.Encoder = (*JournaldEncoder)(nil)
219+
_ caddyfile.Unmarshaler = (*JournaldEncoder)(nil)
220+
_ caddy.ConfiguresFormatterDefault = (*JournaldEncoder)(nil)
221+
)

0 commit comments

Comments
 (0)