Skip to content

Commit c83eea4

Browse files
authored
Merge pull request #84 from mittwald/add/std-log-timestamps
add optional timestamping for stdout logs
2 parents 61096a6 + 8fb8f9f commit c83eea4

File tree

9 files changed

+200
-6
lines changed

9 files changed

+200
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
/dist
55
examples/mittnite.d/local.hcl
66
cmd/mittnitectl/mittnitectl
7+
*.log

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ job "foo" {
154154
}
155155
```
156156
157+
Additionally, you can enable timestamps for the output of a job using `enableTimestamps` and specify a custom format using `timestampFormat`.
158+
159+
Formats are named after their constant name in the Golang [`time` package](https://pkg.go.dev/time#pkg-constants) (lookup table at the bottom).
160+
161+
You can also specify your own format by setting `customTimestampFormat` to a custom format string like "2006-01-02 15:04:05". Whatever is set in `timestampFormat` will be ignored in that case.
162+
163+
```hcl
164+
job "foo" {
165+
command = "/usr/local/bin/foo"
166+
args = ["bar"]
167+
stdout = "/tmp/foo.log"
168+
stderr = "/tmp/foo-errors.log"
169+
enableTimestamps = true
170+
timestampFormat = "RFC3339" # default
171+
customTimestampFormat = "" # default
172+
}
173+
```
174+
157175
You can configure a Job to watch files and to send a signal to the managed process if that file changes. This can be used, for example, to send a `SIGHUP` to a process to reload its configuration file when it changes.
158176
159177
```hcl
@@ -480,3 +498,27 @@ job webserver {
480498
# ...
481499
}
482500
```
501+
502+
### Timestamp Formats
503+
504+
| Name | Format |
505+
|-------------|-------------------------------------|
506+
| Layout | 01/02 03:04:05PM '06 -0700 |
507+
| ANSIC | Mon Jan _2 15:04:05 2006 |
508+
| UnixDate | Mon Jan _2 15:04:05 MST 2006 |
509+
| RubyDate | Mon Jan 02 15:04:05 -0700 2006 |
510+
| RFC822 | 02 Jan 06 15:04 MST |
511+
| RFC822Z | 02 Jan 06 15:04 -0700 |
512+
| RFC850 | Monday, 02-Jan-06 15:04:05 MST |
513+
| RFC1123 | Mon, 02 Jan 2006 15:04:05 MST |
514+
| RFC1123Z | Mon, 02 Jan 2006 15:04:05 -0700 |
515+
| RFC3339 | 2006-01-02T15:04:05Z07:00 |
516+
| RFC3339Nano | 2006-01-02T15:04:05.999999999Z07:00 |
517+
| Kitchen | 3:04PM |
518+
| Stamp | Jan _2 15:04:05 |
519+
| StampMilli | Jan _2 15:04:05.000 |
520+
| StampMicro | Jan _2 15:04:05.000000 |
521+
| StampNano | Jan _2 15:04:05.000000000 |
522+
| DateTime | 2006-01-02 15:04:05 |
523+
| DateOnly | 2006-01-02 |
524+
| TimeOnly | 15:04:05 |

examples/oneshots.d/job.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
3+
sleep 10
4+
exit 0

examples/oneshots.d/job2.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
3+
sleep 5
4+
exit 0

examples/oneshots.d/one_shot.hcl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
boot "oneshot" {
2+
command = "/bin/bash"
3+
args = [
4+
"examples/bootjob.d/job.sh"
5+
]
6+
}
7+
8+
boot "oneshot_two" {
9+
command = "/bin/bash"
10+
args = [
11+
"examples/bootjob.d/job2.sh"
12+
]
13+
}

examples/timestamps.d/timestamps.hcl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
job "echoloop_test" {
2+
command = "/bin/bash"
3+
args = [
4+
"-c",
5+
"while true ; do echo 'test'; sleep 10; done"
6+
]
7+
8+
stdout = "test.log"
9+
stderr = "test_error.log"
10+
enableTimestamps = true
11+
timestampFormat = "test"
12+
}
13+
14+
job "echoloop_custom" {
15+
command = "/bin/bash"
16+
args = [
17+
"-c",
18+
"while true ; do echo 'test'; sleep 10; done"
19+
]
20+
21+
stdout = "test_custom.log"
22+
stderr = "test_custom_error.log"
23+
enableTimestamps = true
24+
customTimestampFormat = "2006-01-02 15:04:05"
25+
}
26+
27+
job "echoloop_kitchentime" {
28+
command = "/bin/bash"
29+
args = [
30+
"-c",
31+
"while true ; do echo 'test'; sleep 10; done"
32+
]
33+
34+
stdout = "test_kitchentime.log"
35+
stderr = "test_kitchentime_error.log"
36+
enableTimestamps = true
37+
timestampFormat = "Kitchen"
38+
}
39+
40+
job "echoloop_notime" {
41+
command = "/bin/bash"
42+
args = [
43+
"-c",
44+
"while true ; do echo 'test'; sleep 10; done"
45+
]
46+
47+
stdout = "test_notime.log"
48+
stderr = "test_notime_error.log"
49+
}

internal/config/types.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,13 @@ type BaseJobConfig struct {
9494
CanFail bool `hcl:"canFail" json:"canFail"`
9595
Controllable bool `hcl:"controllable" json:"controllable"`
9696
WorkingDirectory string `hcl:"workingDirectory" json:"workingDirectory,omitempty"`
97-
Stdout string `hcl:"stdout" json:"stdout,omitempty"`
98-
Stderr string `hcl:"stderr" json:"stderr,omitempty"`
97+
98+
// log config
99+
Stdout string `hcl:"stdout" json:"stdout,omitempty"`
100+
Stderr string `hcl:"stderr" json:"stderr,omitempty"`
101+
EnableTimestamps bool `hcl:"enableTimestamps" json:"enableTimestamps"`
102+
TimestampFormat string `hcl:"timestampFormat" json:"timestampFormat"` // defaults to RFC3339
103+
CustomTimestampFormat string `hcl:"customTimestampFormat" json:"customTimestampFormat"`
99104
}
100105

101106
type Laziness struct {

pkg/proc/basejob.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,29 @@ func (job *baseJob) startOnce(ctx context.Context, process chan<- *os.Process) e
114114
cmd := exec.Command(job.Config.Command, job.Config.Args...)
115115
cmd.Env = os.Environ()
116116
cmd.Dir = job.Config.WorkingDirectory
117-
cmd.Stdout = job.stdout
118-
cmd.Stderr = job.stderr
117+
118+
// pipe command's stdout and stderr through timestamp function if timestamps are enabled
119+
// otherwise just redirect stdout and err to job.stdout and job.stderr
120+
if job.Config.EnableTimestamps {
121+
stdoutPipe, err := cmd.StdoutPipe()
122+
if err != nil {
123+
return fmt.Errorf("failed to create stdout pipe for process: %s", err.Error())
124+
}
125+
defer stdoutPipe.Close()
126+
127+
stderrPipe, err := cmd.StderrPipe()
128+
if err != nil {
129+
return fmt.Errorf("failed to create stderr pipe for process: %s", err.Error())
130+
}
131+
defer stderrPipe.Close()
132+
133+
go job.logWithTimestamp(stdoutPipe, job.stdout)
134+
go job.logWithTimestamp(stderrPipe, job.stderr)
135+
} else {
136+
cmd.Stdout = job.stdout
137+
cmd.Stderr = job.stderr
138+
}
139+
119140
cmd.SysProcAttr = &syscall.SysProcAttr{
120141
Setpgid: true,
121142
}
@@ -201,6 +222,40 @@ func (job *baseJob) closeStdFiles() {
201222
}
202223
}
203224

225+
func (job *baseJob) logWithTimestamp(r io.Reader, w io.Writer) {
226+
l := log.WithField("job.name", job.Config.Name)
227+
228+
var layout string
229+
230+
// has custom timestamp layout?
231+
if job.Config.CustomTimestampFormat != "" {
232+
layout = job.Config.CustomTimestampFormat
233+
l.Infof("using custom timestamp layout '%s'", layout)
234+
} else {
235+
existingLayout, exists := TimeLayouts[job.Config.TimestampFormat]
236+
if !exists {
237+
layout = time.RFC3339
238+
l.Warningf("unknown timestamp layout '%s', defaulting to RFC3339", job.Config.TimestampFormat)
239+
} else {
240+
layout = existingLayout
241+
l.Infof("logging with timestamp layout '%s'", job.Config.TimestampFormat)
242+
}
243+
}
244+
245+
scanner := bufio.NewScanner(r)
246+
for scanner.Scan() {
247+
timestamp := time.Now().Format(layout)
248+
line := fmt.Sprintf("[%s] %s\n", timestamp, scanner.Text())
249+
if _, err := w.Write([]byte(line)); err != nil {
250+
l.Errorf("error writing log for process: %v\n", err)
251+
}
252+
}
253+
254+
if err := scanner.Err(); err != nil {
255+
l.Errorf("error reading from process: %v\n", err)
256+
}
257+
}
258+
204259
func (job *baseJob) readStdFile(ctx context.Context, wg *sync.WaitGroup, filePath string, outChan chan []byte, errChan chan error, follow bool, tailLen int) {
205260
stdFile, err := os.OpenFile(filePath, os.O_RDONLY, 0o666)
206261
if err != nil {

pkg/proc/types.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,42 @@ package proc
22

33
import (
44
"context"
5-
"github.com/gorilla/mux"
6-
"github.com/gorilla/websocket"
75
"net/http"
86
"os"
97
"os/exec"
108
"sync"
119
"time"
1210

11+
"github.com/gorilla/mux"
12+
"github.com/gorilla/websocket"
13+
1314
"github.com/mittwald/mittnite/internal/config"
1415
)
1516

1617
const (
1718
ShutdownWaitingTimeSeconds = 10
1819
)
1920

21+
var TimeLayouts = map[string]string{
22+
"RFC3339": time.RFC3339,
23+
"RFC3339Nano": time.RFC3339Nano,
24+
"RFC1123": time.RFC1123,
25+
"RFC1123Z": time.RFC1123Z,
26+
"RFC822": time.RFC822,
27+
"RFC822Z": time.RFC822Z,
28+
"ANSIC": time.ANSIC,
29+
"UnixDate": time.UnixDate,
30+
"RubyDate": time.RubyDate,
31+
"Kitchen": time.Kitchen,
32+
"Stamp": time.Stamp,
33+
"StampMilli": time.StampMilli,
34+
"StampMicro": time.StampMicro,
35+
"StampNano": time.StampNano,
36+
"DateTime": time.DateTime,
37+
"DateOnly": time.DateOnly,
38+
"TimeOnly": time.TimeOnly,
39+
}
40+
2041
type Runner struct {
2142
jobs []Job
2243
bootJobs []*BootJob

0 commit comments

Comments
 (0)