forked from cloudfoundry/bosh-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file_logging_cmd_runner.go
197 lines (158 loc) · 4.34 KB
/
file_logging_cmd_runner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package cmdrunner
import (
"bytes"
"fmt"
"os"
"path"
"unicode/utf8"
bosherr "github.com/cloudfoundry/bosh-utils/errors"
boshsys "github.com/cloudfoundry/bosh-utils/system"
)
const (
fileOpenFlag int = os.O_RDWR | os.O_CREATE | os.O_TRUNC
fileOpenPerm os.FileMode = os.FileMode(0640)
)
type FileLoggingCmdRunner struct {
fs boshsys.FileSystem
cmdRunner boshsys.CmdRunner
baseDir string
truncateLength int64
}
type FileLoggingExecErr struct {
result *CmdResult
}
func (f FileLoggingExecErr) Error() string {
stdoutTitle := "Stdout"
if f.result.IsStdoutTruncated {
stdoutTitle = "Truncated stdout"
}
stderrTitle := "Stderr"
if f.result.IsStderrTruncated {
stderrTitle = "Truncated stderr"
}
return fmt.Sprintf("Command exited with %d; %s: %s, %s: %s",
f.result.ExitStatus,
stdoutTitle,
f.result.Stdout,
stderrTitle,
f.result.Stderr,
)
}
func NewFileLoggingCmdRunner(
fs boshsys.FileSystem,
cmdRunner boshsys.CmdRunner,
baseDir string,
truncateLength int64,
) CmdRunner {
return FileLoggingCmdRunner{
fs: fs,
cmdRunner: cmdRunner,
baseDir: baseDir,
truncateLength: truncateLength,
}
}
func (f FileLoggingCmdRunner) RunCommand(jobName string, taskName string, cmd boshsys.Command) (*CmdResult, error) {
logsDir := path.Join(f.baseDir, jobName)
err := f.fs.RemoveAll(logsDir)
if err != nil {
return nil, bosherr.WrapErrorf(err, "Removing log dir for job %s", jobName)
}
err = f.fs.MkdirAll(logsDir, os.FileMode(0750))
if err != nil {
return nil, bosherr.WrapErrorf(err, "Creating log dir for job %s", jobName)
}
stdoutPath := path.Join(logsDir, fmt.Sprintf("%s.stdout.log", taskName))
stderrPath := path.Join(logsDir, fmt.Sprintf("%s.stderr.log", taskName))
stdoutFile, err := f.fs.OpenFile(stdoutPath, fileOpenFlag, fileOpenPerm)
if err != nil {
return nil, bosherr.WrapErrorf(err, "Opening stdout for task %s", taskName)
}
defer func() {
_ = stdoutFile.Close()
}()
cmd.Stdout = stdoutFile
stderrFile, err := f.fs.OpenFile(stderrPath, fileOpenFlag, fileOpenPerm)
if err != nil {
return nil, bosherr.WrapErrorf(err, "Opening stderr for task %s", taskName)
}
defer func() {
_ = stderrFile.Close()
}()
cmd.Stderr = stderrFile
// Stdout/stderr are redirected to the files
_, _, exitStatus, runErr := f.cmdRunner.RunComplexCommand(cmd)
stdout, isStdoutTruncated, err := f.getTruncatedOutput(stdoutFile, f.truncateLength)
if err != nil {
return nil, bosherr.WrapErrorf(err, "Truncating stdout for task %s", taskName)
}
stderr, isStderrTruncated, err := f.getTruncatedOutput(stderrFile, f.truncateLength)
if err != nil {
return nil, bosherr.WrapErrorf(err, "Truncating stderr for task %s", taskName)
}
result := &CmdResult{
IsStdoutTruncated: isStdoutTruncated,
IsStderrTruncated: isStderrTruncated,
Stdout: stdout,
Stderr: stderr,
ExitStatus: exitStatus,
}
if runErr != nil {
return nil, FileLoggingExecErr{result}
}
return result, nil
}
func (f FileLoggingCmdRunner) getTruncatedOutput(file boshsys.File, truncateLength int64) ([]byte, bool, error) {
isTruncated := false
stat, err := file.Stat()
if err != nil {
return nil, false, err
}
resultSize := truncateLength
offset := stat.Size() - truncateLength
if offset < 0 {
resultSize = stat.Size()
offset = 0
} else {
isTruncated = true
}
data := make([]byte, resultSize)
_, err = file.ReadAt(data, offset)
if err != nil {
return nil, false, err
}
// Do not truncate more than 25% of the data
data = f.truncateUntilToken(data, truncateLength/int64(4))
return data, isTruncated, nil
}
func (f FileLoggingCmdRunner) truncateUntilToken(data []byte, dataLossLimit int64) []byte {
var i int64
// Cut off until first line break unless it cuts off more allowed data loss
if i = int64(bytes.IndexByte(data, '\n')); i >= 0 && i <= dataLossLimit {
data = f.dropCR(data[i+1:])
} else {
// Make sure we don't break inside UTF encoded rune
for {
if len(data) < 1 {
break
}
// Check for ASCII
if data[0] < utf8.RuneSelf {
break
}
// Check for UTF
_, width := utf8.DecodeRune(data)
if width > 1 && utf8.FullRune(data) {
break
}
// Rune is not complete, check next
data = data[1:]
}
}
return data
}
func (f FileLoggingCmdRunner) dropCR(data []byte) []byte {
if len(data) > 0 && data[0] == '\r' {
return data[1:]
}
return data
}