Fixes #1165
Docker-DCO-1.1-Signed-off-by: Alexandr Morozov <lk4d4math@gmail.com> (github: LK4D4)
| ... | ... |
@@ -1583,6 +1583,7 @@ func (cli *DockerCli) CmdDiff(args ...string) error {
|
| 1583 | 1583 |
func (cli *DockerCli) CmdLogs(args ...string) error {
|
| 1584 | 1584 |
cmd := cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container")
|
| 1585 | 1585 |
follow := cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
|
| 1586 |
+ times := cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps")
|
|
| 1586 | 1587 |
if err := cmd.Parse(args); err != nil {
|
| 1587 | 1588 |
return nil |
| 1588 | 1589 |
} |
| ... | ... |
@@ -1603,14 +1604,16 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
|
| 1603 | 1603 |
} |
| 1604 | 1604 |
|
| 1605 | 1605 |
v := url.Values{}
|
| 1606 |
- v.Set("logs", "1")
|
|
| 1607 | 1606 |
v.Set("stdout", "1")
|
| 1608 | 1607 |
v.Set("stderr", "1")
|
| 1608 |
+ if *times {
|
|
| 1609 |
+ v.Set("timestamps", "1")
|
|
| 1610 |
+ } |
|
| 1609 | 1611 |
if *follow && container.State.Running {
|
| 1610 |
- v.Set("stream", "1")
|
|
| 1612 |
+ v.Set("follow", "1")
|
|
| 1611 | 1613 |
} |
| 1612 | 1614 |
|
| 1613 |
- if err := cli.hijack("POST", "/containers/"+name+"/attach?"+v.Encode(), container.Config.Tty, nil, cli.out, cli.err, nil); err != nil {
|
|
| 1615 |
+ if err := cli.streamHelper("GET", "/containers/"+name+"/logs?"+v.Encode(), container.Config.Tty, nil, cli.out, cli.err, nil); err != nil {
|
|
| 1614 | 1616 |
return err |
| 1615 | 1617 |
} |
| 1616 | 1618 |
return nil |
| ... | ... |
@@ -130,6 +130,10 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b
|
| 130 | 130 |
} |
| 131 | 131 |
|
| 132 | 132 |
func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error {
|
| 133 |
+ return cli.streamHelper(method, path, true, in, out, nil, headers) |
|
| 134 |
+} |
|
| 135 |
+ |
|
| 136 |
+func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in io.Reader, stdout, stderr io.Writer, headers map[string][]string) error {
|
|
| 133 | 137 |
if (method == "POST" || method == "PUT") && in == nil {
|
| 134 | 138 |
in = bytes.NewReader([]byte{})
|
| 135 | 139 |
} |
| ... | ... |
@@ -184,9 +188,16 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h |
| 184 | 184 |
} |
| 185 | 185 |
|
| 186 | 186 |
if api.MatchesContentType(resp.Header.Get("Content-Type"), "application/json") {
|
| 187 |
- return utils.DisplayJSONMessagesStream(resp.Body, out, cli.terminalFd, cli.isTerminal) |
|
| 187 |
+ return utils.DisplayJSONMessagesStream(resp.Body, stdout, cli.terminalFd, cli.isTerminal) |
|
| 188 | 188 |
} |
| 189 |
- if _, err := io.Copy(out, resp.Body); err != nil {
|
|
| 189 |
+ if stdout != nil || stderr != nil {
|
|
| 190 |
+ // When TTY is ON, use regular copy |
|
| 191 |
+ if setRawTerminal {
|
|
| 192 |
+ _, err = io.Copy(stdout, resp.Body) |
|
| 193 |
+ } else {
|
|
| 194 |
+ _, err = utils.StdCopy(stdout, stderr, resp.Body) |
|
| 195 |
+ } |
|
| 196 |
+ utils.Debugf("[stream] End of stdout")
|
|
| 190 | 197 |
return err |
| 191 | 198 |
} |
| 192 | 199 |
return nil |
| ... | ... |
@@ -328,6 +328,48 @@ func getContainersJSON(eng *engine.Engine, version version.Version, w http.Respo |
| 328 | 328 |
return nil |
| 329 | 329 |
} |
| 330 | 330 |
|
| 331 |
+func getContainersLogs(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 332 |
+ if err := parseForm(r); err != nil {
|
|
| 333 |
+ return err |
|
| 334 |
+ } |
|
| 335 |
+ if vars == nil {
|
|
| 336 |
+ return fmt.Errorf("Missing parameter")
|
|
| 337 |
+ } |
|
| 338 |
+ |
|
| 339 |
+ var ( |
|
| 340 |
+ job = eng.Job("inspect", vars["name"], "container")
|
|
| 341 |
+ c, err = job.Stdout.AddEnv() |
|
| 342 |
+ ) |
|
| 343 |
+ if err != nil {
|
|
| 344 |
+ return err |
|
| 345 |
+ } |
|
| 346 |
+ if err = job.Run(); err != nil {
|
|
| 347 |
+ return err |
|
| 348 |
+ } |
|
| 349 |
+ |
|
| 350 |
+ var outStream, errStream io.Writer |
|
| 351 |
+ outStream = utils.NewWriteFlusher(w) |
|
| 352 |
+ |
|
| 353 |
+ if c.GetSubEnv("Config") != nil && !c.GetSubEnv("Config").GetBool("Tty") && version.GreaterThanOrEqualTo("1.6") {
|
|
| 354 |
+ errStream = utils.NewStdWriter(outStream, utils.Stderr) |
|
| 355 |
+ outStream = utils.NewStdWriter(outStream, utils.Stdout) |
|
| 356 |
+ } else {
|
|
| 357 |
+ errStream = outStream |
|
| 358 |
+ } |
|
| 359 |
+ |
|
| 360 |
+ job = eng.Job("logs", vars["name"])
|
|
| 361 |
+ job.Setenv("follow", r.Form.Get("follow"))
|
|
| 362 |
+ job.Setenv("stdout", r.Form.Get("stdout"))
|
|
| 363 |
+ job.Setenv("stderr", r.Form.Get("stderr"))
|
|
| 364 |
+ job.Setenv("timestamps", r.Form.Get("timestamps"))
|
|
| 365 |
+ job.Stdout.Add(outStream) |
|
| 366 |
+ job.Stderr.Set(errStream) |
|
| 367 |
+ if err := job.Run(); err != nil {
|
|
| 368 |
+ fmt.Fprintf(outStream, "Error: %s\n", err) |
|
| 369 |
+ } |
|
| 370 |
+ return nil |
|
| 371 |
+} |
|
| 372 |
+ |
|
| 331 | 373 |
func postImagesTag(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 332 | 374 |
if err := parseForm(r); err != nil {
|
| 333 | 375 |
return err |
| ... | ... |
@@ -1017,6 +1059,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st |
| 1017 | 1017 |
"/containers/{name:.*}/changes": getContainersChanges,
|
| 1018 | 1018 |
"/containers/{name:.*}/json": getContainersByName,
|
| 1019 | 1019 |
"/containers/{name:.*}/top": getContainersTop,
|
| 1020 |
+ "/containers/{name:.*}/logs": getContainersLogs,
|
|
| 1020 | 1021 |
"/containers/{name:.*}/attach/ws": wsContainersAttach,
|
| 1021 | 1022 |
}, |
| 1022 | 1023 |
"POST": {
|
| ... | ... |
@@ -473,6 +473,18 @@ func (container *Container) StderrPipe() (io.ReadCloser, error) {
|
| 473 | 473 |
return utils.NewBufReader(reader), nil |
| 474 | 474 |
} |
| 475 | 475 |
|
| 476 |
+func (container *Container) StdoutLogPipe() io.ReadCloser {
|
|
| 477 |
+ reader, writer := io.Pipe() |
|
| 478 |
+ container.stdout.AddWriter(writer, "stdout") |
|
| 479 |
+ return utils.NewBufReader(reader) |
|
| 480 |
+} |
|
| 481 |
+ |
|
| 482 |
+func (container *Container) StderrLogPipe() io.ReadCloser {
|
|
| 483 |
+ reader, writer := io.Pipe() |
|
| 484 |
+ container.stderr.AddWriter(writer, "stderr") |
|
| 485 |
+ return utils.NewBufReader(reader) |
|
| 486 |
+} |
|
| 487 |
+ |
|
| 476 | 488 |
func (container *Container) buildHostnameAndHostsFiles(IP string) {
|
| 477 | 489 |
container.HostnamePath = path.Join(container.root, "hostname") |
| 478 | 490 |
ioutil.WriteFile(container.HostnamePath, []byte(container.Config.Hostname+"\n"), 0644) |
| ... | ... |
@@ -45,6 +45,10 @@ You can still call an old version of the api using |
| 45 | 45 |
You can now use the `-until` parameter to close connection |
| 46 | 46 |
after timestamp. |
| 47 | 47 |
|
| 48 |
+`GET /containers/(id)/logs` |
|
| 49 |
+ |
|
| 50 |
+This url is prefered method for getting container logs now. |
|
| 51 |
+ |
|
| 48 | 52 |
### v1.10 |
| 49 | 53 |
|
| 50 | 54 |
#### Full Documentation |
| ... | ... |
@@ -300,6 +300,42 @@ List processes running inside the container `id` |
| 300 | 300 |
- **404** – no such container |
| 301 | 301 |
- **500** – server error |
| 302 | 302 |
|
| 303 |
+### Get container logs |
|
| 304 |
+ |
|
| 305 |
+`GET /containers/(id)/logs` |
|
| 306 |
+ |
|
| 307 |
+Get stdout and stderr logs from the container ``id`` |
|
| 308 |
+ |
|
| 309 |
+ **Example request**: |
|
| 310 |
+ |
|
| 311 |
+ GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1 HTTP/1.1 |
|
| 312 |
+ |
|
| 313 |
+ **Example response**: |
|
| 314 |
+ |
|
| 315 |
+ HTTP/1.1 200 OK |
|
| 316 |
+ Content-Type: application/vnd.docker.raw-stream |
|
| 317 |
+ |
|
| 318 |
+ {{ STREAM }}
|
|
| 319 |
+ |
|
| 320 |
+ Query Parameters: |
|
| 321 |
+ |
|
| 322 |
+ Â |
|
| 323 |
+ |
|
| 324 |
+ - **follow** – 1/True/true or 0/False/false, return stream. |
|
| 325 |
+ Default false |
|
| 326 |
+ - **stdout** – 1/True/true or 0/False/false, if logs=true, return |
|
| 327 |
+ stdout log. Default false |
|
| 328 |
+ - **stderr** – 1/True/true or 0/False/false, if logs=true, return |
|
| 329 |
+ stderr log. Default false |
|
| 330 |
+ - **timestamps** – 1/True/true or 0/False/false, if logs=true, print |
|
| 331 |
+ timestamps for every log line. Default false |
|
| 332 |
+ |
|
| 333 |
+ Status Codes: |
|
| 334 |
+ |
|
| 335 |
+ - **200** – no error |
|
| 336 |
+ - **404** – no such container |
|
| 337 |
+ - **500** – server error |
|
| 338 |
+ |
|
| 303 | 339 |
### Inspect changes on a container's filesystem |
| 304 | 340 |
|
| 305 | 341 |
`GET /containers/(id)/changes` |
| ... | ... |
@@ -649,13 +649,14 @@ Fetch the logs of a container |
| 649 | 649 |
Usage: docker logs [OPTIONS] CONTAINER |
| 650 | 650 |
|
| 651 | 651 |
-f, --follow=false: Follow log output |
| 652 |
+ -t, --timestamps=false: Show timestamps |
|
| 652 | 653 |
|
| 653 | 654 |
The `docker logs` command batch-retrieves all logs |
| 654 | 655 |
present at the time of execution. |
| 655 | 656 |
|
| 656 |
-The `docker logs --follow` command combines `docker logs` and `docker |
|
| 657 |
-attach`: it will first return all logs from the beginning and then |
|
| 658 |
-continue streaming new output from the container'sstdout and stderr. |
|
| 657 |
+The ``docker logs --follow`` command will first return all logs from the |
|
| 658 |
+beginning and then continue streaming new output from the container's stdout |
|
| 659 |
+and stderr. |
|
| 659 | 660 |
|
| 660 | 661 |
## port |
| 661 | 662 |
|
| ... | ... |
@@ -3,7 +3,10 @@ package main |
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"os/exec" |
| 6 |
+ "regexp" |
|
| 7 |
+ "strings" |
|
| 6 | 8 |
"testing" |
| 9 |
+ "time" |
|
| 7 | 10 |
) |
| 8 | 11 |
|
| 9 | 12 |
// This used to work, it test a log of PageSize-1 (gh#4851) |
| ... | ... |
@@ -74,3 +77,95 @@ func TestLogsContainerMuchBiggerThanPage(t *testing.T) {
|
| 74 | 74 |
|
| 75 | 75 |
logDone("logs - logs container running echo much bigger than page size")
|
| 76 | 76 |
} |
| 77 |
+ |
|
| 78 |
+func TestLogsTimestamps(t *testing.T) {
|
|
| 79 |
+ testLen := 100 |
|
| 80 |
+ runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo =; done;", testLen))
|
|
| 81 |
+ |
|
| 82 |
+ out, _, _, err := runCommandWithStdoutStderr(runCmd) |
|
| 83 |
+ errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
|
|
| 84 |
+ |
|
| 85 |
+ cleanedContainerID := stripTrailingCharacters(out) |
|
| 86 |
+ exec.Command(dockerBinary, "wait", cleanedContainerID).Run() |
|
| 87 |
+ |
|
| 88 |
+ logsCmd := exec.Command(dockerBinary, "logs", "-t", cleanedContainerID) |
|
| 89 |
+ out, _, _, err = runCommandWithStdoutStderr(logsCmd) |
|
| 90 |
+ errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
|
|
| 91 |
+ |
|
| 92 |
+ lines := strings.Split(out, "\n") |
|
| 93 |
+ |
|
| 94 |
+ if len(lines) != testLen+1 {
|
|
| 95 |
+ t.Fatalf("Expected log %d lines, received %d\n", testLen+1, len(lines))
|
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ ts := regexp.MustCompile(`^\[.*?\]`) |
|
| 99 |
+ |
|
| 100 |
+ for _, l := range lines {
|
|
| 101 |
+ if l != "" {
|
|
| 102 |
+ _, err := time.Parse("["+time.StampMilli+"]", ts.FindString(l))
|
|
| 103 |
+ if err != nil {
|
|
| 104 |
+ t.Fatalf("Failed to parse timestamp from %v: %v", l, err)
|
|
| 105 |
+ } |
|
| 106 |
+ } |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ deleteContainer(cleanedContainerID) |
|
| 110 |
+ |
|
| 111 |
+ logDone("logs - logs with timestamps")
|
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+func TestLogsSeparateStderr(t *testing.T) {
|
|
| 115 |
+ msg := "stderr_log" |
|
| 116 |
+ runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg))
|
|
| 117 |
+ |
|
| 118 |
+ out, _, _, err := runCommandWithStdoutStderr(runCmd) |
|
| 119 |
+ errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
|
|
| 120 |
+ |
|
| 121 |
+ cleanedContainerID := stripTrailingCharacters(out) |
|
| 122 |
+ exec.Command(dockerBinary, "wait", cleanedContainerID).Run() |
|
| 123 |
+ |
|
| 124 |
+ logsCmd := exec.Command(dockerBinary, "logs", cleanedContainerID) |
|
| 125 |
+ stdout, stderr, _, err := runCommandWithStdoutStderr(logsCmd) |
|
| 126 |
+ errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
|
|
| 127 |
+ |
|
| 128 |
+ if stdout != "" {
|
|
| 129 |
+ t.Fatalf("Expected empty stdout stream, got %v", stdout)
|
|
| 130 |
+ } |
|
| 131 |
+ |
|
| 132 |
+ stderr = strings.TrimSpace(stderr) |
|
| 133 |
+ if stderr != msg {
|
|
| 134 |
+ t.Fatalf("Expected %v in stderr stream, got %v", msg, stderr)
|
|
| 135 |
+ } |
|
| 136 |
+ |
|
| 137 |
+ deleteContainer(cleanedContainerID) |
|
| 138 |
+ |
|
| 139 |
+ logDone("logs - separate stderr (without pseudo-tty)")
|
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+func TestLogsStderrInStdout(t *testing.T) {
|
|
| 143 |
+ msg := "stderr_log" |
|
| 144 |
+ runCmd := exec.Command(dockerBinary, "run", "-d", "-t", "busybox", "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg))
|
|
| 145 |
+ |
|
| 146 |
+ out, _, _, err := runCommandWithStdoutStderr(runCmd) |
|
| 147 |
+ errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
|
|
| 148 |
+ |
|
| 149 |
+ cleanedContainerID := stripTrailingCharacters(out) |
|
| 150 |
+ exec.Command(dockerBinary, "wait", cleanedContainerID).Run() |
|
| 151 |
+ |
|
| 152 |
+ logsCmd := exec.Command(dockerBinary, "logs", cleanedContainerID) |
|
| 153 |
+ stdout, stderr, _, err := runCommandWithStdoutStderr(logsCmd) |
|
| 154 |
+ errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
|
|
| 155 |
+ |
|
| 156 |
+ if stderr != "" {
|
|
| 157 |
+ t.Fatalf("Expected empty stderr stream, got %v", stdout)
|
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ stdout = strings.TrimSpace(stdout) |
|
| 161 |
+ if stdout != msg {
|
|
| 162 |
+ t.Fatalf("Expected %v in stdout stream, got %v", msg, stdout)
|
|
| 163 |
+ } |
|
| 164 |
+ |
|
| 165 |
+ deleteContainer(cleanedContainerID) |
|
| 166 |
+ |
|
| 167 |
+ logDone("logs - stderr in stdout (with pseudo-tty)")
|
|
| 168 |
+} |
| ... | ... |
@@ -124,6 +124,7 @@ func InitServer(job *engine.Job) engine.Status {
|
| 124 | 124 |
"container_copy": srv.ContainerCopy, |
| 125 | 125 |
"insert": srv.ImageInsert, |
| 126 | 126 |
"attach": srv.ContainerAttach, |
| 127 |
+ "logs": srv.ContainerLogs, |
|
| 127 | 128 |
"search": srv.ImagesSearch, |
| 128 | 129 |
"changes": srv.ContainerChanges, |
| 129 | 130 |
"top": srv.ContainerTop, |
| ... | ... |
@@ -2252,6 +2253,96 @@ func (srv *Server) ContainerResize(job *engine.Job) engine.Status {
|
| 2252 | 2252 |
return job.Errorf("No such container: %s", name)
|
| 2253 | 2253 |
} |
| 2254 | 2254 |
|
| 2255 |
+func (srv *Server) ContainerLogs(job *engine.Job) engine.Status {
|
|
| 2256 |
+ if len(job.Args) != 1 {
|
|
| 2257 |
+ return job.Errorf("Usage: %s CONTAINER\n", job.Name)
|
|
| 2258 |
+ } |
|
| 2259 |
+ |
|
| 2260 |
+ var ( |
|
| 2261 |
+ name = job.Args[0] |
|
| 2262 |
+ stdout = job.GetenvBool("stdout")
|
|
| 2263 |
+ stderr = job.GetenvBool("stderr")
|
|
| 2264 |
+ follow = job.GetenvBool("follow")
|
|
| 2265 |
+ times = job.GetenvBool("timestamps")
|
|
| 2266 |
+ format string |
|
| 2267 |
+ ) |
|
| 2268 |
+ if !(stdout || stderr) {
|
|
| 2269 |
+ return job.Errorf("You must choose at least one stream")
|
|
| 2270 |
+ } |
|
| 2271 |
+ if times {
|
|
| 2272 |
+ format = time.StampMilli |
|
| 2273 |
+ } |
|
| 2274 |
+ container := srv.daemon.Get(name) |
|
| 2275 |
+ if container == nil {
|
|
| 2276 |
+ return job.Errorf("No such container: %s", name)
|
|
| 2277 |
+ } |
|
| 2278 |
+ cLog, err := container.ReadLog("json")
|
|
| 2279 |
+ if err != nil && os.IsNotExist(err) {
|
|
| 2280 |
+ // Legacy logs |
|
| 2281 |
+ utils.Debugf("Old logs format")
|
|
| 2282 |
+ if stdout {
|
|
| 2283 |
+ cLog, err := container.ReadLog("stdout")
|
|
| 2284 |
+ if err != nil {
|
|
| 2285 |
+ utils.Errorf("Error reading logs (stdout): %s", err)
|
|
| 2286 |
+ } else if _, err := io.Copy(job.Stdout, cLog); err != nil {
|
|
| 2287 |
+ utils.Errorf("Error streaming logs (stdout): %s", err)
|
|
| 2288 |
+ } |
|
| 2289 |
+ } |
|
| 2290 |
+ if stderr {
|
|
| 2291 |
+ cLog, err := container.ReadLog("stderr")
|
|
| 2292 |
+ if err != nil {
|
|
| 2293 |
+ utils.Errorf("Error reading logs (stderr): %s", err)
|
|
| 2294 |
+ } else if _, err := io.Copy(job.Stderr, cLog); err != nil {
|
|
| 2295 |
+ utils.Errorf("Error streaming logs (stderr): %s", err)
|
|
| 2296 |
+ } |
|
| 2297 |
+ } |
|
| 2298 |
+ } else if err != nil {
|
|
| 2299 |
+ utils.Errorf("Error reading logs (json): %s", err)
|
|
| 2300 |
+ } else {
|
|
| 2301 |
+ dec := json.NewDecoder(cLog) |
|
| 2302 |
+ for {
|
|
| 2303 |
+ l := &utils.JSONLog{}
|
|
| 2304 |
+ |
|
| 2305 |
+ if err := dec.Decode(l); err == io.EOF {
|
|
| 2306 |
+ break |
|
| 2307 |
+ } else if err != nil {
|
|
| 2308 |
+ utils.Errorf("Error streaming logs: %s", err)
|
|
| 2309 |
+ break |
|
| 2310 |
+ } |
|
| 2311 |
+ logLine := l.Log |
|
| 2312 |
+ if times {
|
|
| 2313 |
+ logLine = fmt.Sprintf("[%s] %s", l.Created.Format(format), logLine)
|
|
| 2314 |
+ } |
|
| 2315 |
+ if l.Stream == "stdout" && stdout {
|
|
| 2316 |
+ fmt.Fprintf(job.Stdout, "%s", logLine) |
|
| 2317 |
+ } |
|
| 2318 |
+ if l.Stream == "stderr" && stderr {
|
|
| 2319 |
+ fmt.Fprintf(job.Stderr, "%s", logLine) |
|
| 2320 |
+ } |
|
| 2321 |
+ } |
|
| 2322 |
+ } |
|
| 2323 |
+ if follow {
|
|
| 2324 |
+ errors := make(chan error, 2) |
|
| 2325 |
+ if stdout {
|
|
| 2326 |
+ stdoutPipe := container.StdoutLogPipe() |
|
| 2327 |
+ go func() {
|
|
| 2328 |
+ errors <- utils.WriteLog(stdoutPipe, job.Stdout, format) |
|
| 2329 |
+ }() |
|
| 2330 |
+ } |
|
| 2331 |
+ if stderr {
|
|
| 2332 |
+ stderrPipe := container.StderrLogPipe() |
|
| 2333 |
+ go func() {
|
|
| 2334 |
+ errors <- utils.WriteLog(stderrPipe, job.Stderr, format) |
|
| 2335 |
+ }() |
|
| 2336 |
+ } |
|
| 2337 |
+ err := <-errors |
|
| 2338 |
+ if err != nil {
|
|
| 2339 |
+ utils.Errorf("%s", err)
|
|
| 2340 |
+ } |
|
| 2341 |
+ } |
|
| 2342 |
+ return engine.StatusOK |
|
| 2343 |
+} |
|
| 2344 |
+ |
|
| 2255 | 2345 |
func (srv *Server) ContainerAttach(job *engine.Job) engine.Status {
|
| 2256 | 2346 |
if len(job.Args) != 1 {
|
| 2257 | 2347 |
return job.Errorf("Usage: %s CONTAINER\n", job.Name)
|
| ... | ... |
@@ -341,18 +341,15 @@ func (r *bufReader) Close() error {
|
| 341 | 341 |
type WriteBroadcaster struct {
|
| 342 | 342 |
sync.Mutex |
| 343 | 343 |
buf *bytes.Buffer |
| 344 |
- writers map[StreamWriter]bool |
|
| 345 |
-} |
|
| 346 |
- |
|
| 347 |
-type StreamWriter struct {
|
|
| 348 |
- wc io.WriteCloser |
|
| 349 |
- stream string |
|
| 344 |
+ streams map[string](map[io.WriteCloser]struct{})
|
|
| 350 | 345 |
} |
| 351 | 346 |
|
| 352 | 347 |
func (w *WriteBroadcaster) AddWriter(writer io.WriteCloser, stream string) {
|
| 353 | 348 |
w.Lock() |
| 354 |
- sw := StreamWriter{wc: writer, stream: stream}
|
|
| 355 |
- w.writers[sw] = true |
|
| 349 |
+ if _, ok := w.streams[stream]; !ok {
|
|
| 350 |
+ w.streams[stream] = make(map[io.WriteCloser]struct{})
|
|
| 351 |
+ } |
|
| 352 |
+ w.streams[stream][writer] = struct{}{}
|
|
| 356 | 353 |
w.Unlock() |
| 357 | 354 |
} |
| 358 | 355 |
|
| ... | ... |
@@ -362,33 +359,83 @@ type JSONLog struct {
|
| 362 | 362 |
Created time.Time `json:"time"` |
| 363 | 363 |
} |
| 364 | 364 |
|
| 365 |
+func (jl *JSONLog) Format(format string) (string, error) {
|
|
| 366 |
+ if format == "" {
|
|
| 367 |
+ return jl.Log, nil |
|
| 368 |
+ } |
|
| 369 |
+ if format == "json" {
|
|
| 370 |
+ m, err := json.Marshal(jl) |
|
| 371 |
+ return string(m), err |
|
| 372 |
+ } |
|
| 373 |
+ return fmt.Sprintf("[%s] %s", jl.Created.Format(format), jl.Log), nil
|
|
| 374 |
+} |
|
| 375 |
+ |
|
| 376 |
+func WriteLog(src io.Reader, dst io.WriteCloser, format string) error {
|
|
| 377 |
+ dec := json.NewDecoder(src) |
|
| 378 |
+ for {
|
|
| 379 |
+ l := &JSONLog{}
|
|
| 380 |
+ |
|
| 381 |
+ if err := dec.Decode(l); err == io.EOF {
|
|
| 382 |
+ return nil |
|
| 383 |
+ } else if err != nil {
|
|
| 384 |
+ Errorf("Error streaming logs: %s", err)
|
|
| 385 |
+ return err |
|
| 386 |
+ } |
|
| 387 |
+ line, err := l.Format(format) |
|
| 388 |
+ if err != nil {
|
|
| 389 |
+ return err |
|
| 390 |
+ } |
|
| 391 |
+ fmt.Fprintf(dst, "%s", line) |
|
| 392 |
+ } |
|
| 393 |
+} |
|
| 394 |
+ |
|
| 395 |
+type LogFormatter struct {
|
|
| 396 |
+ wc io.WriteCloser |
|
| 397 |
+ timeFormat string |
|
| 398 |
+} |
|
| 399 |
+ |
|
| 365 | 400 |
func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
|
| 401 |
+ created := time.Now().UTC() |
|
| 366 | 402 |
w.Lock() |
| 367 | 403 |
defer w.Unlock() |
| 404 |
+ if writers, ok := w.streams[""]; ok {
|
|
| 405 |
+ for sw := range writers {
|
|
| 406 |
+ if n, err := sw.Write(p); err != nil || n != len(p) {
|
|
| 407 |
+ // On error, evict the writer |
|
| 408 |
+ delete(writers, sw) |
|
| 409 |
+ } |
|
| 410 |
+ } |
|
| 411 |
+ } |
|
| 368 | 412 |
w.buf.Write(p) |
| 369 |
- for sw := range w.writers {
|
|
| 370 |
- lp := p |
|
| 371 |
- if sw.stream != "" {
|
|
| 372 |
- lp = nil |
|
| 373 |
- for {
|
|
| 374 |
- line, err := w.buf.ReadString('\n')
|
|
| 375 |
- if err != nil {
|
|
| 376 |
- w.buf.Write([]byte(line)) |
|
| 377 |
- break |
|
| 378 |
- } |
|
| 379 |
- b, err := json.Marshal(&JSONLog{Log: line, Stream: sw.stream, Created: time.Now().UTC()})
|
|
| 413 |
+ lines := []string{}
|
|
| 414 |
+ for {
|
|
| 415 |
+ line, err := w.buf.ReadString('\n')
|
|
| 416 |
+ if err != nil {
|
|
| 417 |
+ w.buf.Write([]byte(line)) |
|
| 418 |
+ break |
|
| 419 |
+ } |
|
| 420 |
+ lines = append(lines, line) |
|
| 421 |
+ } |
|
| 422 |
+ |
|
| 423 |
+ if len(lines) != 0 {
|
|
| 424 |
+ for stream, writers := range w.streams {
|
|
| 425 |
+ if stream == "" {
|
|
| 426 |
+ continue |
|
| 427 |
+ } |
|
| 428 |
+ var lp []byte |
|
| 429 |
+ for _, line := range lines {
|
|
| 430 |
+ b, err := json.Marshal(&JSONLog{Log: line, Stream: stream, Created: created})
|
|
| 380 | 431 |
if err != nil {
|
| 381 |
- // On error, evict the writer |
|
| 382 |
- delete(w.writers, sw) |
|
| 383 |
- continue |
|
| 432 |
+ Errorf("Error making JSON log line: %s", err)
|
|
| 384 | 433 |
} |
| 385 | 434 |
lp = append(lp, b...) |
| 386 | 435 |
lp = append(lp, '\n') |
| 387 | 436 |
} |
| 388 |
- } |
|
| 389 |
- if n, err := sw.wc.Write(lp); err != nil || n != len(lp) {
|
|
| 390 |
- // On error, evict the writer |
|
| 391 |
- delete(w.writers, sw) |
|
| 437 |
+ for sw := range writers {
|
|
| 438 |
+ if _, err := sw.Write(lp); err != nil {
|
|
| 439 |
+ delete(writers, sw) |
|
| 440 |
+ } |
|
| 441 |
+ } |
|
| 392 | 442 |
} |
| 393 | 443 |
} |
| 394 | 444 |
return len(p), nil |
| ... | ... |
@@ -397,15 +444,20 @@ func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
|
| 397 | 397 |
func (w *WriteBroadcaster) CloseWriters() error {
|
| 398 | 398 |
w.Lock() |
| 399 | 399 |
defer w.Unlock() |
| 400 |
- for sw := range w.writers {
|
|
| 401 |
- sw.wc.Close() |
|
| 400 |
+ for _, writers := range w.streams {
|
|
| 401 |
+ for w := range writers {
|
|
| 402 |
+ w.Close() |
|
| 403 |
+ } |
|
| 402 | 404 |
} |
| 403 |
- w.writers = make(map[StreamWriter]bool) |
|
| 405 |
+ w.streams = make(map[string](map[io.WriteCloser]struct{}))
|
|
| 404 | 406 |
return nil |
| 405 | 407 |
} |
| 406 | 408 |
|
| 407 | 409 |
func NewWriteBroadcaster() *WriteBroadcaster {
|
| 408 |
- return &WriteBroadcaster{writers: make(map[StreamWriter]bool), buf: bytes.NewBuffer(nil)}
|
|
| 410 |
+ return &WriteBroadcaster{
|
|
| 411 |
+ streams: make(map[string](map[io.WriteCloser]struct{})),
|
|
| 412 |
+ buf: bytes.NewBuffer(nil), |
|
| 413 |
+ } |
|
| 409 | 414 |
} |
| 410 | 415 |
|
| 411 | 416 |
func GetTotalUsedFds() int {
|