| ... | ... |
@@ -2,7 +2,9 @@ package swarm |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
basictypes "github.com/docker/docker/api/types" |
| 5 |
+ "github.com/docker/docker/api/types/backend" |
|
| 5 | 6 |
types "github.com/docker/docker/api/types/swarm" |
| 7 |
+ "golang.org/x/net/context" |
|
| 6 | 8 |
) |
| 7 | 9 |
|
| 8 | 10 |
// Backend abstracts an swarm commands manager. |
| ... | ... |
@@ -19,6 +21,7 @@ type Backend interface {
|
| 19 | 19 |
CreateService(types.ServiceSpec, string) (string, error) |
| 20 | 20 |
UpdateService(string, uint64, types.ServiceSpec, string, string) error |
| 21 | 21 |
RemoveService(string) error |
| 22 |
+ ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
|
|
| 22 | 23 |
GetNodes(basictypes.NodeListOptions) ([]types.Node, error) |
| 23 | 24 |
GetNode(string) (types.Node, error) |
| 24 | 25 |
UpdateNode(string, uint64, types.NodeSpec) error |
| ... | ... |
@@ -1,6 +1,9 @@ |
| 1 | 1 |
package swarm |
| 2 | 2 |
|
| 3 |
-import "github.com/docker/docker/api/server/router" |
|
| 3 |
+import ( |
|
| 4 |
+ "github.com/docker/docker/api/server/router" |
|
| 5 |
+ "github.com/docker/docker/daemon" |
|
| 6 |
+) |
|
| 4 | 7 |
|
| 5 | 8 |
// buildRouter is a router to talk with the build controller |
| 6 | 9 |
type swarmRouter struct {
|
| ... | ... |
@@ -9,11 +12,14 @@ type swarmRouter struct {
|
| 9 | 9 |
} |
| 10 | 10 |
|
| 11 | 11 |
// NewRouter initializes a new build router |
| 12 |
-func NewRouter(b Backend) router.Router {
|
|
| 12 |
+func NewRouter(d *daemon.Daemon, b Backend) router.Router {
|
|
| 13 | 13 |
r := &swarmRouter{
|
| 14 | 14 |
backend: b, |
| 15 | 15 |
} |
| 16 | 16 |
r.initRoutes() |
| 17 |
+ if d.HasExperimental() {
|
|
| 18 |
+ r.addExperimentalRoutes() |
|
| 19 |
+ } |
|
| 17 | 20 |
return r |
| 18 | 21 |
} |
| 19 | 22 |
|
| ... | ... |
@@ -22,6 +28,12 @@ func (sr *swarmRouter) Routes() []router.Route {
|
| 22 | 22 |
return sr.routes |
| 23 | 23 |
} |
| 24 | 24 |
|
| 25 |
+func (sr *swarmRouter) addExperimentalRoutes() {
|
|
| 26 |
+ sr.routes = append(sr.routes, |
|
| 27 |
+ router.Cancellable(router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs)),
|
|
| 28 |
+ ) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 25 | 31 |
func (sr *swarmRouter) initRoutes() {
|
| 26 | 32 |
sr.routes = []router.Route{
|
| 27 | 33 |
router.NewPostRoute("/swarm/init", sr.initCluster),
|
| ... | ... |
@@ -32,20 +44,20 @@ func (sr *swarmRouter) initRoutes() {
|
| 32 | 32 |
router.NewPostRoute("/swarm/update", sr.updateCluster),
|
| 33 | 33 |
router.NewPostRoute("/swarm/unlock", sr.unlockCluster),
|
| 34 | 34 |
router.NewGetRoute("/services", sr.getServices),
|
| 35 |
- router.NewGetRoute("/services/{id:.*}", sr.getService),
|
|
| 35 |
+ router.NewGetRoute("/services/{id}", sr.getService),
|
|
| 36 | 36 |
router.NewPostRoute("/services/create", sr.createService),
|
| 37 |
- router.NewPostRoute("/services/{id:.*}/update", sr.updateService),
|
|
| 38 |
- router.NewDeleteRoute("/services/{id:.*}", sr.removeService),
|
|
| 37 |
+ router.NewPostRoute("/services/{id}/update", sr.updateService),
|
|
| 38 |
+ router.NewDeleteRoute("/services/{id}", sr.removeService),
|
|
| 39 | 39 |
router.NewGetRoute("/nodes", sr.getNodes),
|
| 40 |
- router.NewGetRoute("/nodes/{id:.*}", sr.getNode),
|
|
| 41 |
- router.NewDeleteRoute("/nodes/{id:.*}", sr.removeNode),
|
|
| 42 |
- router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
|
|
| 40 |
+ router.NewGetRoute("/nodes/{id}", sr.getNode),
|
|
| 41 |
+ router.NewDeleteRoute("/nodes/{id}", sr.removeNode),
|
|
| 42 |
+ router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
|
|
| 43 | 43 |
router.NewGetRoute("/tasks", sr.getTasks),
|
| 44 |
- router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
|
|
| 44 |
+ router.NewGetRoute("/tasks/{id}", sr.getTask),
|
|
| 45 | 45 |
router.NewGetRoute("/secrets", sr.getSecrets),
|
| 46 | 46 |
router.NewPostRoute("/secrets", sr.createSecret),
|
| 47 |
- router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
|
|
| 48 |
- router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
|
|
| 49 |
- router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
|
|
| 47 |
+ router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),
|
|
| 48 |
+ router.NewGetRoute("/secrets/{id}", sr.getSecret),
|
|
| 49 |
+ router.NewPostRoute("/secrets/{id}/update", sr.updateSecret),
|
|
| 50 | 50 |
} |
| 51 | 51 |
} |
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/api/errors" |
| 11 | 11 |
"github.com/docker/docker/api/server/httputils" |
| 12 | 12 |
basictypes "github.com/docker/docker/api/types" |
| 13 |
+ "github.com/docker/docker/api/types/backend" |
|
| 13 | 14 |
"github.com/docker/docker/api/types/filters" |
| 14 | 15 |
types "github.com/docker/docker/api/types/swarm" |
| 15 | 16 |
"golang.org/x/net/context" |
| ... | ... |
@@ -208,6 +209,59 @@ func (sr *swarmRouter) removeService(ctx context.Context, w http.ResponseWriter, |
| 208 | 208 |
return nil |
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 |
+func (sr *swarmRouter) getServiceLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 212 |
+ if err := httputils.ParseForm(r); err != nil {
|
|
| 213 |
+ return err |
|
| 214 |
+ } |
|
| 215 |
+ |
|
| 216 |
+ // Args are validated before the stream starts because when it starts we're |
|
| 217 |
+ // sending HTTP 200 by writing an empty chunk of data to tell the client that |
|
| 218 |
+ // daemon is going to stream. By sending this initial HTTP 200 we can't report |
|
| 219 |
+ // any error after the stream starts (i.e. container not found, wrong parameters) |
|
| 220 |
+ // with the appropriate status code. |
|
| 221 |
+ stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr") |
|
| 222 |
+ if !(stdout || stderr) {
|
|
| 223 |
+ return fmt.Errorf("Bad parameters: you must choose at least one stream")
|
|
| 224 |
+ } |
|
| 225 |
+ |
|
| 226 |
+ serviceName := vars["id"] |
|
| 227 |
+ logsConfig := &backend.ContainerLogsConfig{
|
|
| 228 |
+ ContainerLogsOptions: basictypes.ContainerLogsOptions{
|
|
| 229 |
+ Follow: httputils.BoolValue(r, "follow"), |
|
| 230 |
+ Timestamps: httputils.BoolValue(r, "timestamps"), |
|
| 231 |
+ Since: r.Form.Get("since"),
|
|
| 232 |
+ Tail: r.Form.Get("tail"),
|
|
| 233 |
+ ShowStdout: stdout, |
|
| 234 |
+ ShowStderr: stderr, |
|
| 235 |
+ Details: httputils.BoolValue(r, "details"), |
|
| 236 |
+ }, |
|
| 237 |
+ OutStream: w, |
|
| 238 |
+ } |
|
| 239 |
+ |
|
| 240 |
+ if !logsConfig.Follow {
|
|
| 241 |
+ return fmt.Errorf("Bad parameters: Only follow mode is currently supported")
|
|
| 242 |
+ } |
|
| 243 |
+ |
|
| 244 |
+ if logsConfig.Details {
|
|
| 245 |
+ return fmt.Errorf("Bad parameters: details is not currently supported")
|
|
| 246 |
+ } |
|
| 247 |
+ |
|
| 248 |
+ chStarted := make(chan struct{})
|
|
| 249 |
+ if err := sr.backend.ServiceLogs(ctx, serviceName, logsConfig, chStarted); err != nil {
|
|
| 250 |
+ select {
|
|
| 251 |
+ case <-chStarted: |
|
| 252 |
+ // The client may be expecting all of the data we're sending to |
|
| 253 |
+ // be multiplexed, so send it through OutStream, which will |
|
| 254 |
+ // have been set up to handle that if needed. |
|
| 255 |
+ fmt.Fprintf(logsConfig.OutStream, "Error grabbing service logs: %v\n", err) |
|
| 256 |
+ default: |
|
| 257 |
+ return err |
|
| 258 |
+ } |
|
| 259 |
+ } |
|
| 260 |
+ |
|
| 261 |
+ return nil |
|
| 262 |
+} |
|
| 263 |
+ |
|
| 211 | 264 |
func (sr *swarmRouter) getNodes(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 212 | 265 |
if err := httputils.ParseForm(r); err != nil {
|
| 213 | 266 |
return err |
| 32 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,163 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/types" |
|
| 11 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 12 |
+ "github.com/docker/docker/cli" |
|
| 13 |
+ "github.com/docker/docker/cli/command" |
|
| 14 |
+ "github.com/docker/docker/cli/command/idresolver" |
|
| 15 |
+ "github.com/docker/docker/pkg/stdcopy" |
|
| 16 |
+ "github.com/spf13/cobra" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+type logsOptions struct {
|
|
| 20 |
+ noResolve bool |
|
| 21 |
+ follow bool |
|
| 22 |
+ since string |
|
| 23 |
+ timestamps bool |
|
| 24 |
+ details bool |
|
| 25 |
+ tail string |
|
| 26 |
+ |
|
| 27 |
+ service string |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
| 31 |
+ var opts logsOptions |
|
| 32 |
+ |
|
| 33 |
+ cmd := &cobra.Command{
|
|
| 34 |
+ Use: "logs [OPTIONS] SERVICE", |
|
| 35 |
+ Short: "Fetch the logs of a service", |
|
| 36 |
+ Args: cli.ExactArgs(1), |
|
| 37 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 38 |
+ opts.service = args[0] |
|
| 39 |
+ return runLogs(dockerCli, &opts) |
|
| 40 |
+ }, |
|
| 41 |
+ Tags: map[string]string{"experimental": ""},
|
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ flags := cmd.Flags() |
|
| 45 |
+ flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") |
|
| 46 |
+ flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") |
|
| 47 |
+ flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") |
|
| 48 |
+ flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") |
|
| 49 |
+ flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") |
|
| 50 |
+ flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") |
|
| 51 |
+ return cmd |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
|
| 55 |
+ ctx := context.Background() |
|
| 56 |
+ |
|
| 57 |
+ options := types.ContainerLogsOptions{
|
|
| 58 |
+ ShowStdout: true, |
|
| 59 |
+ ShowStderr: true, |
|
| 60 |
+ Since: opts.since, |
|
| 61 |
+ Timestamps: opts.timestamps, |
|
| 62 |
+ Follow: opts.follow, |
|
| 63 |
+ Tail: opts.tail, |
|
| 64 |
+ Details: opts.details, |
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ client := dockerCli.Client() |
|
| 68 |
+ responseBody, err := client.ServiceLogs(ctx, opts.service, options) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ return err |
|
| 71 |
+ } |
|
| 72 |
+ defer responseBody.Close() |
|
| 73 |
+ |
|
| 74 |
+ resolver := idresolver.New(client, opts.noResolve) |
|
| 75 |
+ |
|
| 76 |
+ stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()}
|
|
| 77 |
+ stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()}
|
|
| 78 |
+ |
|
| 79 |
+ // TODO(aluzzardi): Do an io.Copy for services with TTY enabled. |
|
| 80 |
+ _, err = stdcopy.StdCopy(stdout, stderr, responseBody) |
|
| 81 |
+ return err |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+type logWriter struct {
|
|
| 85 |
+ ctx context.Context |
|
| 86 |
+ opts *logsOptions |
|
| 87 |
+ r *idresolver.IDResolver |
|
| 88 |
+ w io.Writer |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+func (lw *logWriter) Write(buf []byte) (int, error) {
|
|
| 92 |
+ contextIndex := 0 |
|
| 93 |
+ numParts := 2 |
|
| 94 |
+ if lw.opts.timestamps {
|
|
| 95 |
+ contextIndex++ |
|
| 96 |
+ numParts++ |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ parts := bytes.SplitN(buf, []byte(" "), numParts)
|
|
| 100 |
+ if len(parts) != numParts {
|
|
| 101 |
+ return 0, fmt.Errorf("invalid context in log message: %v", string(buf))
|
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ taskName, nodeName, err := lw.parseContext(string(parts[contextIndex])) |
|
| 105 |
+ if err != nil {
|
|
| 106 |
+ return 0, err |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ output := []byte{}
|
|
| 110 |
+ for i, part := range parts {
|
|
| 111 |
+ // First part doesn't get space separation. |
|
| 112 |
+ if i > 0 {
|
|
| 113 |
+ output = append(output, []byte(" ")...)
|
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ if i == contextIndex {
|
|
| 117 |
+ // TODO(aluzzardi): Consider constant padding. |
|
| 118 |
+ output = append(output, []byte(fmt.Sprintf("%s@%s |", taskName, nodeName))...)
|
|
| 119 |
+ } else {
|
|
| 120 |
+ output = append(output, part...) |
|
| 121 |
+ } |
|
| 122 |
+ } |
|
| 123 |
+ _, err = lw.w.Write(output) |
|
| 124 |
+ if err != nil {
|
|
| 125 |
+ return 0, err |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ return len(buf), nil |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+func (lw *logWriter) parseContext(input string) (string, string, error) {
|
|
| 132 |
+ context := make(map[string]string) |
|
| 133 |
+ |
|
| 134 |
+ components := strings.Split(input, ",") |
|
| 135 |
+ for _, component := range components {
|
|
| 136 |
+ parts := strings.SplitN(component, "=", 2) |
|
| 137 |
+ if len(parts) != 2 {
|
|
| 138 |
+ return "", "", fmt.Errorf("invalid context: %s", input)
|
|
| 139 |
+ } |
|
| 140 |
+ context[parts[0]] = parts[1] |
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+ taskID, ok := context["com.docker.swarm.task.id"] |
|
| 144 |
+ if !ok {
|
|
| 145 |
+ return "", "", fmt.Errorf("missing task id in context: %s", input)
|
|
| 146 |
+ } |
|
| 147 |
+ taskName, err := lw.r.Resolve(lw.ctx, swarm.Task{}, taskID)
|
|
| 148 |
+ if err != nil {
|
|
| 149 |
+ return "", "", err |
|
| 150 |
+ } |
|
| 151 |
+ |
|
| 152 |
+ nodeID, ok := context["com.docker.swarm.node.id"] |
|
| 153 |
+ if !ok {
|
|
| 154 |
+ return "", "", fmt.Errorf("missing node id in context: %s", input)
|
|
| 155 |
+ } |
|
| 156 |
+ nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID)
|
|
| 157 |
+ if err != nil {
|
|
| 158 |
+ return "", "", err |
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ return taskName, nodeName, nil |
|
| 162 |
+} |
| ... | ... |
@@ -111,6 +111,7 @@ type ServiceAPIClient interface {
|
| 111 | 111 |
ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) |
| 112 | 112 |
ServiceRemove(ctx context.Context, serviceID string) error |
| 113 | 113 |
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error |
| 114 |
+ ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) |
|
| 114 | 115 |
TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) |
| 115 | 116 |
TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) |
| 116 | 117 |
} |
| 117 | 118 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,52 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "time" |
|
| 6 |
+ |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/types" |
|
| 10 |
+ timetypes "github.com/docker/docker/api/types/time" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ServiceLogs returns the logs generated by a service in an io.ReadCloser. |
|
| 14 |
+// It's up to the caller to close the stream. |
|
| 15 |
+func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
|
| 16 |
+ query := url.Values{}
|
|
| 17 |
+ if options.ShowStdout {
|
|
| 18 |
+ query.Set("stdout", "1")
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ if options.ShowStderr {
|
|
| 22 |
+ query.Set("stderr", "1")
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ if options.Since != "" {
|
|
| 26 |
+ ts, err := timetypes.GetTimestamp(options.Since, time.Now()) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return nil, err |
|
| 29 |
+ } |
|
| 30 |
+ query.Set("since", ts)
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ if options.Timestamps {
|
|
| 34 |
+ query.Set("timestamps", "1")
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ if options.Details {
|
|
| 38 |
+ query.Set("details", "1")
|
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ if options.Follow {
|
|
| 42 |
+ query.Set("follow", "1")
|
|
| 43 |
+ } |
|
| 44 |
+ query.Set("tail", options.Tail)
|
|
| 45 |
+ |
|
| 46 |
+ resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return nil, err |
|
| 49 |
+ } |
|
| 50 |
+ return resp.body, nil |
|
| 51 |
+} |
| 0 | 52 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,133 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "log" |
|
| 8 |
+ "net/http" |
|
| 9 |
+ "os" |
|
| 10 |
+ "strings" |
|
| 11 |
+ "testing" |
|
| 12 |
+ "time" |
|
| 13 |
+ |
|
| 14 |
+ "github.com/docker/docker/api/types" |
|
| 15 |
+ |
|
| 16 |
+ "golang.org/x/net/context" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+func TestServiceLogsError(t *testing.T) {
|
|
| 20 |
+ client := &Client{
|
|
| 21 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 22 |
+ } |
|
| 23 |
+ _, err := client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{})
|
|
| 24 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 25 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 26 |
+ } |
|
| 27 |
+ _, err = client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{
|
|
| 28 |
+ Since: "2006-01-02TZ", |
|
| 29 |
+ }) |
|
| 30 |
+ if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) {
|
|
| 31 |
+ t.Fatalf("expected a 'parsing time' error, got %v", err)
|
|
| 32 |
+ } |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func TestServiceLogs(t *testing.T) {
|
|
| 36 |
+ expectedURL := "/services/service_id/logs" |
|
| 37 |
+ cases := []struct {
|
|
| 38 |
+ options types.ContainerLogsOptions |
|
| 39 |
+ expectedQueryParams map[string]string |
|
| 40 |
+ }{
|
|
| 41 |
+ {
|
|
| 42 |
+ expectedQueryParams: map[string]string{
|
|
| 43 |
+ "tail": "", |
|
| 44 |
+ }, |
|
| 45 |
+ }, |
|
| 46 |
+ {
|
|
| 47 |
+ options: types.ContainerLogsOptions{
|
|
| 48 |
+ Tail: "any", |
|
| 49 |
+ }, |
|
| 50 |
+ expectedQueryParams: map[string]string{
|
|
| 51 |
+ "tail": "any", |
|
| 52 |
+ }, |
|
| 53 |
+ }, |
|
| 54 |
+ {
|
|
| 55 |
+ options: types.ContainerLogsOptions{
|
|
| 56 |
+ ShowStdout: true, |
|
| 57 |
+ ShowStderr: true, |
|
| 58 |
+ Timestamps: true, |
|
| 59 |
+ Details: true, |
|
| 60 |
+ Follow: true, |
|
| 61 |
+ }, |
|
| 62 |
+ expectedQueryParams: map[string]string{
|
|
| 63 |
+ "tail": "", |
|
| 64 |
+ "stdout": "1", |
|
| 65 |
+ "stderr": "1", |
|
| 66 |
+ "timestamps": "1", |
|
| 67 |
+ "details": "1", |
|
| 68 |
+ "follow": "1", |
|
| 69 |
+ }, |
|
| 70 |
+ }, |
|
| 71 |
+ {
|
|
| 72 |
+ options: types.ContainerLogsOptions{
|
|
| 73 |
+ // An complete invalid date, timestamp or go duration will be |
|
| 74 |
+ // passed as is |
|
| 75 |
+ Since: "invalid but valid", |
|
| 76 |
+ }, |
|
| 77 |
+ expectedQueryParams: map[string]string{
|
|
| 78 |
+ "tail": "", |
|
| 79 |
+ "since": "invalid but valid", |
|
| 80 |
+ }, |
|
| 81 |
+ }, |
|
| 82 |
+ } |
|
| 83 |
+ for _, logCase := range cases {
|
|
| 84 |
+ client := &Client{
|
|
| 85 |
+ client: newMockClient(func(r *http.Request) (*http.Response, error) {
|
|
| 86 |
+ if !strings.HasPrefix(r.URL.Path, expectedURL) {
|
|
| 87 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
|
|
| 88 |
+ } |
|
| 89 |
+ // Check query parameters |
|
| 90 |
+ query := r.URL.Query() |
|
| 91 |
+ for key, expected := range logCase.expectedQueryParams {
|
|
| 92 |
+ actual := query.Get(key) |
|
| 93 |
+ if actual != expected {
|
|
| 94 |
+ return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
|
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ return &http.Response{
|
|
| 98 |
+ StatusCode: http.StatusOK, |
|
| 99 |
+ Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
|
|
| 100 |
+ }, nil |
|
| 101 |
+ }), |
|
| 102 |
+ } |
|
| 103 |
+ body, err := client.ServiceLogs(context.Background(), "service_id", logCase.options) |
|
| 104 |
+ if err != nil {
|
|
| 105 |
+ t.Fatal(err) |
|
| 106 |
+ } |
|
| 107 |
+ defer body.Close() |
|
| 108 |
+ content, err := ioutil.ReadAll(body) |
|
| 109 |
+ if err != nil {
|
|
| 110 |
+ t.Fatal(err) |
|
| 111 |
+ } |
|
| 112 |
+ if string(content) != "response" {
|
|
| 113 |
+ t.Fatalf("expected response to contain 'response', got %s", string(content))
|
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+func ExampleClient_ServiceLogs_withTimeout() {
|
|
| 119 |
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
| 120 |
+ defer cancel() |
|
| 121 |
+ |
|
| 122 |
+ client, _ := NewEnvClient() |
|
| 123 |
+ reader, err := client.ServiceLogs(ctx, "service_id", types.ContainerLogsOptions{})
|
|
| 124 |
+ if err != nil {
|
|
| 125 |
+ log.Fatal(err) |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ _, err = io.Copy(os.Stdout, reader) |
|
| 129 |
+ if err != nil && err != io.EOF {
|
|
| 130 |
+ log.Fatal(err) |
|
| 131 |
+ } |
|
| 132 |
+} |
| ... | ... |
@@ -456,7 +456,7 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
|
| 456 | 456 |
systemrouter.NewRouter(d, c), |
| 457 | 457 |
volume.NewRouter(d), |
| 458 | 458 |
build.NewRouter(dockerfile.NewBuildManager(d)), |
| 459 |
- swarmrouter.NewRouter(c), |
|
| 459 |
+ swarmrouter.NewRouter(d, c), |
|
| 460 | 460 |
}...) |
| 461 | 461 |
|
| 462 | 462 |
if d.NetworkControllerEnabled() {
|
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"encoding/base64" |
| 5 | 5 |
"encoding/json" |
| 6 | 6 |
"fmt" |
| 7 |
+ "io" |
|
| 7 | 8 |
"io/ioutil" |
| 8 | 9 |
"net" |
| 9 | 10 |
"os" |
| ... | ... |
@@ -16,20 +17,24 @@ import ( |
| 16 | 16 |
"github.com/Sirupsen/logrus" |
| 17 | 17 |
apierrors "github.com/docker/docker/api/errors" |
| 18 | 18 |
apitypes "github.com/docker/docker/api/types" |
| 19 |
+ "github.com/docker/docker/api/types/backend" |
|
| 19 | 20 |
"github.com/docker/docker/api/types/filters" |
| 20 | 21 |
"github.com/docker/docker/api/types/network" |
| 21 | 22 |
types "github.com/docker/docker/api/types/swarm" |
| 22 | 23 |
"github.com/docker/docker/daemon/cluster/convert" |
| 23 | 24 |
executorpkg "github.com/docker/docker/daemon/cluster/executor" |
| 24 | 25 |
"github.com/docker/docker/daemon/cluster/executor/container" |
| 26 |
+ "github.com/docker/docker/daemon/logger" |
|
| 25 | 27 |
"github.com/docker/docker/opts" |
| 26 | 28 |
"github.com/docker/docker/pkg/ioutils" |
| 27 | 29 |
"github.com/docker/docker/pkg/signal" |
| 30 |
+ "github.com/docker/docker/pkg/stdcopy" |
|
| 28 | 31 |
"github.com/docker/docker/reference" |
| 29 | 32 |
"github.com/docker/docker/runconfig" |
| 30 | 33 |
swarmapi "github.com/docker/swarmkit/api" |
| 31 | 34 |
"github.com/docker/swarmkit/manager/encryption" |
| 32 | 35 |
swarmnode "github.com/docker/swarmkit/node" |
| 36 |
+ "github.com/docker/swarmkit/protobuf/ptypes" |
|
| 33 | 37 |
"github.com/pkg/errors" |
| 34 | 38 |
"golang.org/x/net/context" |
| 35 | 39 |
"google.golang.org/grpc" |
| ... | ... |
@@ -45,6 +50,7 @@ const defaultAddr = "0.0.0.0:2377" |
| 45 | 45 |
const ( |
| 46 | 46 |
initialReconnectDelay = 100 * time.Millisecond |
| 47 | 47 |
maxReconnectDelay = 30 * time.Second |
| 48 |
+ contextPrefix = "com.docker.swarm" |
|
| 48 | 49 |
) |
| 49 | 50 |
|
| 50 | 51 |
// ErrNoSwarm is returned on leaving a cluster that was never initialized |
| ... | ... |
@@ -120,6 +126,7 @@ type node struct {
|
| 120 | 120 |
ready bool |
| 121 | 121 |
conn *grpc.ClientConn |
| 122 | 122 |
client swarmapi.ControlClient |
| 123 |
+ logs swarmapi.LogsClient |
|
| 123 | 124 |
reconnectDelay time.Duration |
| 124 | 125 |
config nodeStartConfig |
| 125 | 126 |
} |
| ... | ... |
@@ -371,8 +378,10 @@ func (c *Cluster) startNewNode(conf nodeStartConfig) (*node, error) {
|
| 371 | 371 |
if node.conn != conn {
|
| 372 | 372 |
if conn == nil {
|
| 373 | 373 |
node.client = nil |
| 374 |
+ node.logs = nil |
|
| 374 | 375 |
} else {
|
| 375 | 376 |
node.client = swarmapi.NewControlClient(conn) |
| 377 |
+ node.logs = swarmapi.NewLogsClient(conn) |
|
| 376 | 378 |
} |
| 377 | 379 |
} |
| 378 | 380 |
node.conn = conn |
| ... | ... |
@@ -1205,6 +1214,88 @@ func (c *Cluster) RemoveService(input string) error {
|
| 1205 | 1205 |
return nil |
| 1206 | 1206 |
} |
| 1207 | 1207 |
|
| 1208 |
+// ServiceLogs collects service logs and writes them back to `config.OutStream` |
|
| 1209 |
+func (c *Cluster) ServiceLogs(ctx context.Context, input string, config *backend.ContainerLogsConfig, started chan struct{}) error {
|
|
| 1210 |
+ c.RLock() |
|
| 1211 |
+ if !c.isActiveManager() {
|
|
| 1212 |
+ c.RUnlock() |
|
| 1213 |
+ return c.errNoManager() |
|
| 1214 |
+ } |
|
| 1215 |
+ |
|
| 1216 |
+ service, err := getService(ctx, c.client, input) |
|
| 1217 |
+ if err != nil {
|
|
| 1218 |
+ c.RUnlock() |
|
| 1219 |
+ return err |
|
| 1220 |
+ } |
|
| 1221 |
+ |
|
| 1222 |
+ stream, err := c.logs.SubscribeLogs(ctx, &swarmapi.SubscribeLogsRequest{
|
|
| 1223 |
+ Selector: &swarmapi.LogSelector{
|
|
| 1224 |
+ ServiceIDs: []string{service.ID},
|
|
| 1225 |
+ }, |
|
| 1226 |
+ Options: &swarmapi.LogSubscriptionOptions{
|
|
| 1227 |
+ Follow: true, |
|
| 1228 |
+ }, |
|
| 1229 |
+ }) |
|
| 1230 |
+ if err != nil {
|
|
| 1231 |
+ c.RUnlock() |
|
| 1232 |
+ return err |
|
| 1233 |
+ } |
|
| 1234 |
+ |
|
| 1235 |
+ wf := ioutils.NewWriteFlusher(config.OutStream) |
|
| 1236 |
+ defer wf.Close() |
|
| 1237 |
+ close(started) |
|
| 1238 |
+ wf.Flush() |
|
| 1239 |
+ |
|
| 1240 |
+ outStream := stdcopy.NewStdWriter(wf, stdcopy.Stdout) |
|
| 1241 |
+ errStream := stdcopy.NewStdWriter(wf, stdcopy.Stderr) |
|
| 1242 |
+ |
|
| 1243 |
+ // Release the lock before starting the stream. |
|
| 1244 |
+ c.RUnlock() |
|
| 1245 |
+ for {
|
|
| 1246 |
+ // Check the context before doing anything. |
|
| 1247 |
+ select {
|
|
| 1248 |
+ case <-ctx.Done(): |
|
| 1249 |
+ return ctx.Err() |
|
| 1250 |
+ default: |
|
| 1251 |
+ } |
|
| 1252 |
+ |
|
| 1253 |
+ subscribeMsg, err := stream.Recv() |
|
| 1254 |
+ if err == io.EOF {
|
|
| 1255 |
+ return nil |
|
| 1256 |
+ } |
|
| 1257 |
+ if err != nil {
|
|
| 1258 |
+ return err |
|
| 1259 |
+ } |
|
| 1260 |
+ |
|
| 1261 |
+ for _, msg := range subscribeMsg.Messages {
|
|
| 1262 |
+ data := []byte{}
|
|
| 1263 |
+ |
|
| 1264 |
+ if config.Timestamps {
|
|
| 1265 |
+ ts, err := ptypes.Timestamp(msg.Timestamp) |
|
| 1266 |
+ if err != nil {
|
|
| 1267 |
+ return err |
|
| 1268 |
+ } |
|
| 1269 |
+ data = append(data, []byte(ts.Format(logger.TimeFormat)+" ")...) |
|
| 1270 |
+ } |
|
| 1271 |
+ |
|
| 1272 |
+ data = append(data, []byte(fmt.Sprintf("%s.node.id=%s,%s.service.id=%s,%s.task.id=%s ",
|
|
| 1273 |
+ contextPrefix, msg.Context.NodeID, |
|
| 1274 |
+ contextPrefix, msg.Context.ServiceID, |
|
| 1275 |
+ contextPrefix, msg.Context.TaskID, |
|
| 1276 |
+ ))...) |
|
| 1277 |
+ |
|
| 1278 |
+ data = append(data, msg.Data...) |
|
| 1279 |
+ |
|
| 1280 |
+ switch msg.Stream {
|
|
| 1281 |
+ case swarmapi.LogStreamStdout: |
|
| 1282 |
+ outStream.Write(data) |
|
| 1283 |
+ case swarmapi.LogStreamStderr: |
|
| 1284 |
+ errStream.Write(data) |
|
| 1285 |
+ } |
|
| 1286 |
+ } |
|
| 1287 |
+ } |
|
| 1288 |
+} |
|
| 1289 |
+ |
|
| 1208 | 1290 |
// GetNodes returns a list of all nodes known to a cluster. |
| 1209 | 1291 |
func (c *Cluster) GetNodes(options apitypes.NodeListOptions) ([]types.Node, error) {
|
| 1210 | 1292 |
c.RLock() |
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
|
| 7 | 7 |
"github.com/docker/distribution" |
| 8 | 8 |
"github.com/docker/docker/api/types" |
| 9 |
+ "github.com/docker/docker/api/types/backend" |
|
| 9 | 10 |
"github.com/docker/docker/api/types/container" |
| 10 | 11 |
"github.com/docker/docker/api/types/events" |
| 11 | 12 |
"github.com/docker/docker/api/types/filters" |
| ... | ... |
@@ -28,6 +29,7 @@ type Backend interface {
|
| 28 | 28 |
CreateManagedContainer(config types.ContainerCreateConfig, validateHostname bool) (container.ContainerCreateCreatedBody, error) |
| 29 | 29 |
ContainerStart(name string, hostConfig *container.HostConfig, validateHostname bool, checkpoint string, checkpointDir string) error |
| 30 | 30 |
ContainerStop(name string, seconds *int) error |
| 31 |
+ ContainerLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
|
|
| 31 | 32 |
ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error |
| 32 | 33 |
ActivateContainerServiceBinding(containerName string) error |
| 33 | 34 |
DeactivateContainerServiceBinding(containerName string) error |
| ... | ... |
@@ -12,6 +12,7 @@ import ( |
| 12 | 12 |
"github.com/Sirupsen/logrus" |
| 13 | 13 |
"github.com/docker/docker/api/server/httputils" |
| 14 | 14 |
"github.com/docker/docker/api/types" |
| 15 |
+ "github.com/docker/docker/api/types/backend" |
|
| 15 | 16 |
containertypes "github.com/docker/docker/api/types/container" |
| 16 | 17 |
"github.com/docker/docker/api/types/events" |
| 17 | 18 |
"github.com/docker/docker/api/types/versions" |
| ... | ... |
@@ -20,6 +21,7 @@ import ( |
| 20 | 20 |
"github.com/docker/swarmkit/agent/exec" |
| 21 | 21 |
"github.com/docker/swarmkit/api" |
| 22 | 22 |
"github.com/docker/swarmkit/log" |
| 23 |
+ "github.com/docker/swarmkit/protobuf/ptypes" |
|
| 23 | 24 |
"golang.org/x/net/context" |
| 24 | 25 |
"golang.org/x/time/rate" |
| 25 | 26 |
) |
| ... | ... |
@@ -376,6 +378,56 @@ func (c *containerAdapter) deactivateServiceBinding() error {
|
| 376 | 376 |
return c.backend.DeactivateContainerServiceBinding(c.container.name()) |
| 377 | 377 |
} |
| 378 | 378 |
|
| 379 |
+func (c *containerAdapter) logs(ctx context.Context, options api.LogSubscriptionOptions) (io.ReadCloser, error) {
|
|
| 380 |
+ reader, writer := io.Pipe() |
|
| 381 |
+ |
|
| 382 |
+ apiOptions := &backend.ContainerLogsConfig{
|
|
| 383 |
+ ContainerLogsOptions: types.ContainerLogsOptions{
|
|
| 384 |
+ Follow: options.Follow, |
|
| 385 |
+ |
|
| 386 |
+ // TODO(stevvooe): Parse timestamp out of message. This |
|
| 387 |
+ // absolutely needs to be done before going to production with |
|
| 388 |
+ // this, at it is completely redundant. |
|
| 389 |
+ Timestamps: true, |
|
| 390 |
+ Details: false, // no clue what to do with this, let's just deprecate it. |
|
| 391 |
+ }, |
|
| 392 |
+ OutStream: writer, |
|
| 393 |
+ } |
|
| 394 |
+ |
|
| 395 |
+ if options.Since != nil {
|
|
| 396 |
+ since, err := ptypes.Timestamp(options.Since) |
|
| 397 |
+ if err != nil {
|
|
| 398 |
+ return nil, err |
|
| 399 |
+ } |
|
| 400 |
+ apiOptions.Since = since.Format(time.RFC3339Nano) |
|
| 401 |
+ } |
|
| 402 |
+ |
|
| 403 |
+ if options.Tail < 0 {
|
|
| 404 |
+ // See protobuf documentation for details of how this works. |
|
| 405 |
+ apiOptions.Tail = fmt.Sprint(-options.Tail - 1) |
|
| 406 |
+ } else if options.Tail > 0 {
|
|
| 407 |
+ return nil, fmt.Errorf("tail relative to start of logs not supported via docker API")
|
|
| 408 |
+ } |
|
| 409 |
+ |
|
| 410 |
+ if len(options.Streams) == 0 {
|
|
| 411 |
+ // empty == all |
|
| 412 |
+ apiOptions.ShowStdout, apiOptions.ShowStderr = true, true |
|
| 413 |
+ } else {
|
|
| 414 |
+ for _, stream := range options.Streams {
|
|
| 415 |
+ switch stream {
|
|
| 416 |
+ case api.LogStreamStdout: |
|
| 417 |
+ apiOptions.ShowStdout = true |
|
| 418 |
+ case api.LogStreamStderr: |
|
| 419 |
+ apiOptions.ShowStderr = true |
|
| 420 |
+ } |
|
| 421 |
+ } |
|
| 422 |
+ } |
|
| 423 |
+ |
|
| 424 |
+ chStarted := make(chan struct{})
|
|
| 425 |
+ go c.backend.ContainerLogs(ctx, c.container.name(), apiOptions, chStarted) |
|
| 426 |
+ return reader, nil |
|
| 427 |
+} |
|
| 428 |
+ |
|
| 379 | 429 |
// todo: typed/wrapped errors |
| 380 | 430 |
func isContainerCreateNameConflict(err error) bool {
|
| 381 | 431 |
return strings.Contains(err.Error(), "Conflict. The name") |
| ... | ... |
@@ -1,8 +1,13 @@ |
| 1 | 1 |
package container |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "bufio" |
|
| 5 |
+ "bytes" |
|
| 6 |
+ "encoding/binary" |
|
| 4 | 7 |
"fmt" |
| 8 |
+ "io" |
|
| 5 | 9 |
"os" |
| 10 |
+ "time" |
|
| 6 | 11 |
|
| 7 | 12 |
"github.com/docker/docker/api/types" |
| 8 | 13 |
"github.com/docker/docker/api/types/events" |
| ... | ... |
@@ -11,8 +16,10 @@ import ( |
| 11 | 11 |
"github.com/docker/swarmkit/agent/exec" |
| 12 | 12 |
"github.com/docker/swarmkit/api" |
| 13 | 13 |
"github.com/docker/swarmkit/log" |
| 14 |
+ "github.com/docker/swarmkit/protobuf/ptypes" |
|
| 14 | 15 |
"github.com/pkg/errors" |
| 15 | 16 |
"golang.org/x/net/context" |
| 17 |
+ "golang.org/x/time/rate" |
|
| 16 | 18 |
) |
| 17 | 19 |
|
| 18 | 20 |
// controller implements agent.Controller against docker's API. |
| ... | ... |
@@ -374,6 +381,128 @@ func (r *controller) Remove(ctx context.Context) error {
|
| 374 | 374 |
return nil |
| 375 | 375 |
} |
| 376 | 376 |
|
| 377 |
+// waitReady waits for a container to be "ready". |
|
| 378 |
+// Ready means it's past the started state. |
|
| 379 |
+func (r *controller) waitReady(pctx context.Context) error {
|
|
| 380 |
+ if err := r.checkClosed(); err != nil {
|
|
| 381 |
+ return err |
|
| 382 |
+ } |
|
| 383 |
+ |
|
| 384 |
+ ctx, cancel := context.WithCancel(pctx) |
|
| 385 |
+ defer cancel() |
|
| 386 |
+ |
|
| 387 |
+ eventq := r.adapter.events(ctx) |
|
| 388 |
+ |
|
| 389 |
+ ctnr, err := r.adapter.inspect(ctx) |
|
| 390 |
+ if err != nil {
|
|
| 391 |
+ if !isUnknownContainer(err) {
|
|
| 392 |
+ return errors.Wrap(err, "inspect container failed") |
|
| 393 |
+ } |
|
| 394 |
+ } else {
|
|
| 395 |
+ switch ctnr.State.Status {
|
|
| 396 |
+ case "running", "exited", "dead": |
|
| 397 |
+ return nil |
|
| 398 |
+ } |
|
| 399 |
+ } |
|
| 400 |
+ |
|
| 401 |
+ for {
|
|
| 402 |
+ select {
|
|
| 403 |
+ case event := <-eventq: |
|
| 404 |
+ if !r.matchevent(event) {
|
|
| 405 |
+ continue |
|
| 406 |
+ } |
|
| 407 |
+ |
|
| 408 |
+ switch event.Action {
|
|
| 409 |
+ case "start": |
|
| 410 |
+ return nil |
|
| 411 |
+ } |
|
| 412 |
+ case <-ctx.Done(): |
|
| 413 |
+ return ctx.Err() |
|
| 414 |
+ case <-r.closed: |
|
| 415 |
+ return r.err |
|
| 416 |
+ } |
|
| 417 |
+ } |
|
| 418 |
+} |
|
| 419 |
+ |
|
| 420 |
+func (r *controller) Logs(ctx context.Context, publisher exec.LogPublisher, options api.LogSubscriptionOptions) error {
|
|
| 421 |
+ if err := r.checkClosed(); err != nil {
|
|
| 422 |
+ return err |
|
| 423 |
+ } |
|
| 424 |
+ |
|
| 425 |
+ if err := r.waitReady(ctx); err != nil {
|
|
| 426 |
+ return errors.Wrap(err, "container not ready for logs") |
|
| 427 |
+ } |
|
| 428 |
+ |
|
| 429 |
+ rc, err := r.adapter.logs(ctx, options) |
|
| 430 |
+ if err != nil {
|
|
| 431 |
+ return errors.Wrap(err, "failed getting container logs") |
|
| 432 |
+ } |
|
| 433 |
+ defer rc.Close() |
|
| 434 |
+ |
|
| 435 |
+ var ( |
|
| 436 |
+ // use a rate limiter to keep things under control but also provides some |
|
| 437 |
+ // ability coalesce messages. |
|
| 438 |
+ limiter = rate.NewLimiter(rate.Every(time.Second), 10<<20) // 10 MB/s |
|
| 439 |
+ msgctx = api.LogContext{
|
|
| 440 |
+ NodeID: r.task.NodeID, |
|
| 441 |
+ ServiceID: r.task.ServiceID, |
|
| 442 |
+ TaskID: r.task.ID, |
|
| 443 |
+ } |
|
| 444 |
+ ) |
|
| 445 |
+ |
|
| 446 |
+ brd := bufio.NewReader(rc) |
|
| 447 |
+ for {
|
|
| 448 |
+ // so, message header is 8 bytes, treat as uint64, pull stream off MSB |
|
| 449 |
+ var header uint64 |
|
| 450 |
+ if err := binary.Read(brd, binary.BigEndian, &header); err != nil {
|
|
| 451 |
+ if err == io.EOF {
|
|
| 452 |
+ return nil |
|
| 453 |
+ } |
|
| 454 |
+ |
|
| 455 |
+ return errors.Wrap(err, "failed reading log header") |
|
| 456 |
+ } |
|
| 457 |
+ |
|
| 458 |
+ stream, size := (header>>(7<<3))&0xFF, header & ^(uint64(0xFF)<<(7<<3)) |
|
| 459 |
+ |
|
| 460 |
+ // limit here to decrease allocation back pressure. |
|
| 461 |
+ if err := limiter.WaitN(ctx, int(size)); err != nil {
|
|
| 462 |
+ return errors.Wrap(err, "failed rate limiter") |
|
| 463 |
+ } |
|
| 464 |
+ |
|
| 465 |
+ buf := make([]byte, size) |
|
| 466 |
+ _, err := io.ReadFull(brd, buf) |
|
| 467 |
+ if err != nil {
|
|
| 468 |
+ return errors.Wrap(err, "failed reading buffer") |
|
| 469 |
+ } |
|
| 470 |
+ |
|
| 471 |
+ // Timestamp is RFC3339Nano with 1 space after. Lop, parse, publish |
|
| 472 |
+ parts := bytes.SplitN(buf, []byte(" "), 2)
|
|
| 473 |
+ if len(parts) != 2 {
|
|
| 474 |
+ return fmt.Errorf("invalid timestamp in log message: %v", buf)
|
|
| 475 |
+ } |
|
| 476 |
+ |
|
| 477 |
+ ts, err := time.Parse(time.RFC3339Nano, string(parts[0])) |
|
| 478 |
+ if err != nil {
|
|
| 479 |
+ return errors.Wrap(err, "failed to parse timestamp") |
|
| 480 |
+ } |
|
| 481 |
+ |
|
| 482 |
+ tsp, err := ptypes.TimestampProto(ts) |
|
| 483 |
+ if err != nil {
|
|
| 484 |
+ return errors.Wrap(err, "failed to convert timestamp") |
|
| 485 |
+ } |
|
| 486 |
+ |
|
| 487 |
+ if err := publisher.Publish(ctx, api.LogMessage{
|
|
| 488 |
+ Context: msgctx, |
|
| 489 |
+ Timestamp: tsp, |
|
| 490 |
+ Stream: api.LogStream(stream), |
|
| 491 |
+ |
|
| 492 |
+ Data: parts[1], |
|
| 493 |
+ }); err != nil {
|
|
| 494 |
+ return errors.Wrap(err, "failed to publish log message") |
|
| 495 |
+ } |
|
| 496 |
+ } |
|
| 497 |
+} |
|
| 498 |
+ |
|
| 377 | 499 |
// Close the runner and clean up any ephemeral resources. |
| 378 | 500 |
func (r *controller) Close() error {
|
| 379 | 501 |
select {
|
| ... | ... |
@@ -149,7 +149,7 @@ This section lists each version from latest to oldest. Each listing includes a |
| 149 | 149 |
* `POST /containers/create` now takes `AutoRemove` in HostConfig, to enable auto-removal of the container on daemon side when the container's process exits. |
| 150 | 150 |
* `GET /containers/json` and `GET /containers/(id or name)/json` now return `"removing"` as a value for the `State.Status` field if the container is being removed. Previously, "exited" was returned as status. |
| 151 | 151 |
* `GET /containers/json` now accepts `removing` as a valid value for the `status` filter. |
| 152 |
-* `GET /containers/json` now supports filtering containers by `health` status. |
|
| 152 |
+* `GET /containers/json` now supports filtering containers by `health` status. |
|
| 153 | 153 |
* `DELETE /volumes/(name)` now accepts a `force` query parameter to force removal of volumes that were already removed out of band by the volume driver plugin. |
| 154 | 154 |
* `POST /containers/create/` and `POST /containers/(name)/update` now validates restart policies. |
| 155 | 155 |
* `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`). |
| ... | ... |
@@ -5631,6 +5631,49 @@ image](#create-an-image) section for more details. |
| 5631 | 5631 |
- **404** – no such service |
| 5632 | 5632 |
- **500** – server error |
| 5633 | 5633 |
|
| 5634 |
+### Get service logs |
|
| 5635 |
+ |
|
| 5636 |
+`GET /services/(id or name)/logs` |
|
| 5637 |
+ |
|
| 5638 |
+Get `stdout` and `stderr` logs from the service ``id`` |
|
| 5639 |
+ |
|
| 5640 |
+> **Note**: |
|
| 5641 |
+> This endpoint works only for services with the `json-file` or `journald` logging drivers. |
|
| 5642 |
+ |
|
| 5643 |
+**Example request**: |
|
| 5644 |
+ |
|
| 5645 |
+ GET /services/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10&since=1428990821 HTTP/1.1 |
|
| 5646 |
+ |
|
| 5647 |
+**Example response**: |
|
| 5648 |
+ |
|
| 5649 |
+ HTTP/1.1 101 UPGRADED |
|
| 5650 |
+ Content-Type: application/vnd.docker.raw-stream |
|
| 5651 |
+ Connection: Upgrade |
|
| 5652 |
+ Upgrade: tcp |
|
| 5653 |
+ |
|
| 5654 |
+ {% raw %}
|
|
| 5655 |
+ {{ STREAM }}
|
|
| 5656 |
+ {% endraw %}
|
|
| 5657 |
+ |
|
| 5658 |
+**Query parameters**: |
|
| 5659 |
+ |
|
| 5660 |
+- **details** - 1/True/true or 0/False/flase, Show extra details provided to logs. Default `false`. |
|
| 5661 |
+- **follow** – 1/True/true or 0/False/false, return stream. Default `false`. |
|
| 5662 |
+- **stdout** – 1/True/true or 0/False/false, show `stdout` log. Default `false`. |
|
| 5663 |
+- **stderr** – 1/True/true or 0/False/false, show `stderr` log. Default `false`. |
|
| 5664 |
+- **since** – UNIX timestamp (integer) to filter logs. Specifying a timestamp |
|
| 5665 |
+ will only output log-entries since that timestamp. Default: 0 (unfiltered) |
|
| 5666 |
+- **timestamps** – 1/True/true or 0/False/false, print timestamps for |
|
| 5667 |
+ every log line. Default `false`. |
|
| 5668 |
+- **tail** – Output specified number of lines at the end of logs: `all` or `<number>`. Default all. |
|
| 5669 |
+ |
|
| 5670 |
+**Status codes**: |
|
| 5671 |
+ |
|
| 5672 |
+- **101** – no error, hints proxy about hijacking |
|
| 5673 |
+- **200** – no error, no upgrade header found |
|
| 5674 |
+- **404** – no such service |
|
| 5675 |
+- **500** – server error |
|
| 5676 |
+ |
|
| 5634 | 5677 |
## 3.10 Tasks |
| 5635 | 5678 |
|
| 5636 | 5679 |
**Note**: Task operations require the engine to be part of a swarm. |
| 5637 | 5680 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,67 @@ |
| 0 |
+--- |
|
| 1 |
+title: "service logs (experimental)" |
|
| 2 |
+description: "The service logs command description and usage" |
|
| 3 |
+keywords: "service, logs" |
|
| 4 |
+advisory: "experimental" |
|
| 5 |
+--- |
|
| 6 |
+ |
|
| 7 |
+<!-- This file is maintained within the docker/docker Github |
|
| 8 |
+ repository at https://github.com/docker/docker/. Make all |
|
| 9 |
+ pull requests against that repo. If you see this file in |
|
| 10 |
+ another repository, consider it read-only there, as it will |
|
| 11 |
+ periodically be overwritten by the definitive file. Pull |
|
| 12 |
+ requests which include edits to this file in other repositories |
|
| 13 |
+ will be rejected. |
|
| 14 |
+--> |
|
| 15 |
+ |
|
| 16 |
+# service logs |
|
| 17 |
+ |
|
| 18 |
+```Markdown |
|
| 19 |
+Usage: docker service logs [OPTIONS] SERVICE |
|
| 20 |
+ |
|
| 21 |
+Fetch the logs of a service |
|
| 22 |
+ |
|
| 23 |
+Options: |
|
| 24 |
+ --details Show extra details provided to logs |
|
| 25 |
+ -f, --follow Follow log output |
|
| 26 |
+ --help Print usage |
|
| 27 |
+ --since string Show logs since timestamp |
|
| 28 |
+ --tail string Number of lines to show from the end of the logs (default "all") |
|
| 29 |
+ -t, --timestamps Show timestamps |
|
| 30 |
+``` |
|
| 31 |
+ |
|
| 32 |
+The `docker service logs` command batch-retrieves logs present at the time of execution. |
|
| 33 |
+ |
|
| 34 |
+> **Note**: this command is only functional for services that are started with |
|
| 35 |
+> the `json-file` or `journald` logging driver. |
|
| 36 |
+ |
|
| 37 |
+For more information about selecting and configuring login-drivers, refer to |
|
| 38 |
+[Configure logging drivers](https://docs.docker.com/engine/admin/logging/overview/). |
|
| 39 |
+ |
|
| 40 |
+The `docker service logs --follow` command will continue streaming the new output from |
|
| 41 |
+the service's `STDOUT` and `STDERR`. |
|
| 42 |
+ |
|
| 43 |
+Passing a negative number or a non-integer to `--tail` is invalid and the |
|
| 44 |
+value is set to `all` in that case. |
|
| 45 |
+ |
|
| 46 |
+The `docker service logs --timestamps` command will add an [RFC3339Nano timestamp](https://golang.org/pkg/time/#pkg-constants) |
|
| 47 |
+, for example `2014-09-16T06:17:46.000000000Z`, to each |
|
| 48 |
+log entry. To ensure that the timestamps are aligned the |
|
| 49 |
+nano-second part of the timestamp will be padded with zero when necessary. |
|
| 50 |
+ |
|
| 51 |
+The `docker service logs --details` command will add on extra attributes, such as |
|
| 52 |
+environment variables and labels, provided to `--log-opt` when creating the |
|
| 53 |
+service. |
|
| 54 |
+ |
|
| 55 |
+The `--since` option shows only the service logs generated after |
|
| 56 |
+a given date. You can specify the date as an RFC 3339 date, a UNIX |
|
| 57 |
+timestamp, or a Go duration string (e.g. `1m30s`, `3h`). Besides RFC3339 date |
|
| 58 |
+format you may also use RFC3339Nano, `2006-01-02T15:04:05`, |
|
| 59 |
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local |
|
| 60 |
+timezone on the client will be used if you do not provide either a `Z` or a |
|
| 61 |
+`+-00:00` timezone offset at the end of the timestamp. When providing Unix |
|
| 62 |
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds |
|
| 63 |
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap |
|
| 64 |
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a |
|
| 65 |
+fraction of a second no more than nine digits long. You can combine the |
|
| 66 |
+`--since` option with either or both of the `--follow` or `--tail` options. |
| 0 | 67 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,55 @@ |
| 0 |
+// +build !windows |
|
| 1 |
+ |
|
| 2 |
+package main |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "bufio" |
|
| 6 |
+ "io" |
|
| 7 |
+ "os/exec" |
|
| 8 |
+ "strings" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/pkg/integration/checker" |
|
| 11 |
+ "github.com/go-check/check" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+type logMessage struct {
|
|
| 15 |
+ err error |
|
| 16 |
+ data []byte |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+func (s *DockerSwarmSuite) TestServiceLogs(c *check.C) {
|
|
| 20 |
+ testRequires(c, ExperimentalDaemon) |
|
| 21 |
+ |
|
| 22 |
+ d := s.AddDaemon(c, true, true) |
|
| 23 |
+ |
|
| 24 |
+ name := "TestServiceLogs" |
|
| 25 |
+ |
|
| 26 |
+ out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "while true; do echo log test; sleep 1; done")
|
|
| 27 |
+ c.Assert(err, checker.IsNil) |
|
| 28 |
+ c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") |
|
| 29 |
+ |
|
| 30 |
+ // make sure task has been deployed. |
|
| 31 |
+ waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) |
|
| 32 |
+ |
|
| 33 |
+ args := []string{"service", "logs", "-f", name}
|
|
| 34 |
+ cmd := exec.Command(dockerBinary, d.prependHostArg(args)...) |
|
| 35 |
+ r, w := io.Pipe() |
|
| 36 |
+ cmd.Stdout = w |
|
| 37 |
+ cmd.Stderr = w |
|
| 38 |
+ c.Assert(cmd.Start(), checker.IsNil) |
|
| 39 |
+ |
|
| 40 |
+ // Make sure pipe is written to |
|
| 41 |
+ ch := make(chan *logMessage) |
|
| 42 |
+ go func() {
|
|
| 43 |
+ reader := bufio.NewReader(r) |
|
| 44 |
+ msg := &logMessage{}
|
|
| 45 |
+ msg.data, _, msg.err = reader.ReadLine() |
|
| 46 |
+ ch <- msg |
|
| 47 |
+ }() |
|
| 48 |
+ |
|
| 49 |
+ msg := <-ch |
|
| 50 |
+ c.Assert(msg.err, checker.IsNil) |
|
| 51 |
+ c.Assert(string(msg.data), checker.Contains, "log test") |
|
| 52 |
+ |
|
| 53 |
+ c.Assert(cmd.Process.Kill(), checker.IsNil) |
|
| 54 |
+} |