1// Package chezmoilog contains support for chezmoi logging.
2package chezmoilog
3
4import (
5	"errors"
6	"net/http"
7	"os"
8	"os/exec"
9	"time"
10
11	"github.com/rs/zerolog"
12	"github.com/rs/zerolog/log"
13)
14
15// An OSExecCmdLogObject wraps an *os/exec.Cmd and adds
16// github.com/rs/zerolog.LogObjectMarshaler functionality.
17type OSExecCmdLogObject struct {
18	*exec.Cmd
19}
20
21// An OSExecExitErrorLogObject wraps an error and adds
22// github.com/rs/zerolog.LogObjectMarshaler functionality if the wrapped error
23// is an os/exec.ExitError.
24type OSExecExitErrorLogObject struct {
25	Err error
26}
27
28// An OSProcessStateLogObject wraps an *os.ProcessState and adds
29// github.com/rs/zerolog.LogObjectMarshaler functionality.
30type OSProcessStateLogObject struct {
31	*os.ProcessState
32}
33
34// MarshalZerologObject implements
35// github.com/rs/zerolog.LogObjectMarshaler.MarshalZerologObject.
36func (cmd OSExecCmdLogObject) MarshalZerologObject(event *zerolog.Event) {
37	if cmd.Cmd == nil {
38		return
39	}
40	if cmd.Path != "" {
41		event.Str("path", cmd.Path)
42	}
43	if cmd.Args != nil {
44		event.Strs("args", cmd.Args)
45	}
46	if cmd.Dir != "" {
47		event.Str("dir", cmd.Dir)
48	}
49	if cmd.Env != nil {
50		event.Strs("env", cmd.Env)
51	}
52}
53
54// MarshalZerologObject implements
55// github.com/rs/zerolog.LogObjectMarshaler.MarshalZerologObject.
56func (err OSExecExitErrorLogObject) MarshalZerologObject(event *zerolog.Event) {
57	if err.Err == nil {
58		return
59	}
60	var osExecExitError *exec.ExitError
61	if !errors.As(err.Err, &osExecExitError) {
62		return
63	}
64	event.EmbedObject(OSProcessStateLogObject{osExecExitError.ProcessState})
65	if osExecExitError.Stderr != nil {
66		event.Bytes("stderr", osExecExitError.Stderr)
67	}
68}
69
70// MarshalZerologObject implements
71// github.com/rs/zerolog.LogObjectMarshaler.MarshalZerologObject.
72func (p OSProcessStateLogObject) MarshalZerologObject(event *zerolog.Event) {
73	if p.ProcessState == nil {
74		return
75	}
76	if p.Exited() {
77		if !p.Success() {
78			event.Int("exitCode", p.ExitCode())
79		}
80	} else {
81		event.Int("pid", p.Pid())
82	}
83	if userTime := p.UserTime(); userTime != 0 {
84		event.Dur("userTime", userTime)
85	}
86	if systemTime := p.SystemTime(); systemTime != 0 {
87		event.Dur("systemTime", systemTime)
88	}
89}
90
91// FirstFewBytes returns the first few bytes of data in a human-readable form.
92func FirstFewBytes(data []byte) []byte {
93	const few = 64
94	if len(data) > few {
95		data = append([]byte{}, data[:few]...)
96		data = append(data, '.', '.', '.')
97	}
98	return data
99}
100
101// LogHTTPRequest calls httpClient.Do, logs the result to logger, and returns
102// the result.
103func LogHTTPRequest(logger *zerolog.Logger, client *http.Client, req *http.Request) (*http.Response, error) {
104	start := time.Now()
105	resp, err := client.Do(req)
106	if resp != nil {
107		logger.Err(err).
108			Stringer("duration", time.Since(start)).
109			Str("method", req.Method).
110			Int64("size", resp.ContentLength).
111			Int("statusCode", resp.StatusCode).
112			Str("status", resp.Status).
113			Stringer("url", req.URL).
114			Msg("HTTPRequest")
115	} else {
116		logger.Err(err).
117			Stringer("duration", time.Since(start)).
118			Str("method", req.Method).
119			Int64("size", resp.ContentLength).
120			Stringer("url", req.URL).
121			Msg("HTTPRequest")
122	}
123	return resp, err
124}
125
126// LogCmdCombinedOutput calls cmd.CombinedOutput, logs the result, and returns the result.
127func LogCmdCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
128	start := time.Now()
129	combinedOutput, err := cmd.CombinedOutput()
130	log.Err(err).
131		EmbedObject(OSExecCmdLogObject{Cmd: cmd}).
132		EmbedObject(OSExecExitErrorLogObject{Err: err}).
133		Bytes("combinedOutput", Output(combinedOutput, err)).
134		Stringer("duration", time.Since(start)).
135		Int("size", len(combinedOutput)).
136		Msg("CombinedOutput")
137	return combinedOutput, err
138}
139
140// LogCmdOutput calls cmd.Output, logs the result, and returns the result.
141func LogCmdOutput(cmd *exec.Cmd) ([]byte, error) {
142	start := time.Now()
143	output, err := cmd.Output()
144	log.Err(err).
145		EmbedObject(OSExecCmdLogObject{Cmd: cmd}).
146		EmbedObject(OSExecExitErrorLogObject{Err: err}).
147		Stringer("duration", time.Since(start)).
148		Bytes("output", Output(output, err)).
149		Int("size", len(output)).
150		Msg("Output")
151	return output, err
152}
153
154// LogCmdRun calls cmd.Run, logs the result, and returns the result.
155func LogCmdRun(cmd *exec.Cmd) error {
156	start := time.Now()
157	err := cmd.Run()
158	log.Err(err).
159		EmbedObject(OSExecCmdLogObject{Cmd: cmd}).
160		EmbedObject(OSExecExitErrorLogObject{Err: err}).
161		Stringer("duration", time.Since(start)).
162		Msg("Run")
163	return err
164}
165
166// Output returns the first few bytes of output if err is nil, otherwise it
167// returns the full output.
168func Output(data []byte, err error) []byte {
169	if err != nil {
170		return data
171	}
172	return FirstFewBytes(data)
173}
174