github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/api/client.go (about)

     1  // Copyright 2013, 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/juju/errors"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/version"
    20  	"golang.org/x/net/websocket"
    21  	"gopkg.in/juju/charm.v6-unstable"
    22  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    23  	"gopkg.in/juju/names.v2"
    24  	"gopkg.in/macaroon.v1"
    25  
    26  	"github.com/juju/juju/api/base"
    27  	"github.com/juju/juju/apiserver/params"
    28  	"github.com/juju/juju/constraints"
    29  	"github.com/juju/juju/downloader"
    30  	"github.com/juju/juju/network"
    31  	"github.com/juju/juju/status"
    32  	"github.com/juju/juju/tools"
    33  )
    34  
    35  // Client represents the client-accessible part of the state.
    36  type Client struct {
    37  	base.ClientFacade
    38  	facade base.FacadeCaller
    39  	st     *state
    40  }
    41  
    42  // Status returns the status of the juju model.
    43  func (c *Client) Status(patterns []string) (*params.FullStatus, error) {
    44  	var result params.FullStatus
    45  	p := params.StatusParams{Patterns: patterns}
    46  	if err := c.facade.FacadeCall("FullStatus", p, &result); err != nil {
    47  		return nil, err
    48  	}
    49  	return &result, nil
    50  }
    51  
    52  // StatusHistory retrieves the last <size> results of
    53  // <kind:combined|agent|workload|machine|machineinstance|container|containerinstance> status
    54  // for <name> unit
    55  func (c *Client) StatusHistory(kind status.HistoryKind, tag names.Tag, filter status.StatusHistoryFilter) (status.History, error) {
    56  	var results params.StatusHistoryResults
    57  	args := params.StatusHistoryRequest{
    58  		Kind: string(kind),
    59  		Filter: params.StatusHistoryFilter{
    60  			Size:  filter.Size,
    61  			Date:  filter.Date,
    62  			Delta: filter.Delta,
    63  		},
    64  		Tag: tag.String(),
    65  	}
    66  	bulkArgs := params.StatusHistoryRequests{Requests: []params.StatusHistoryRequest{args}}
    67  	err := c.facade.FacadeCall("StatusHistory", bulkArgs, &results)
    68  	if err != nil {
    69  		return status.History{}, errors.Trace(err)
    70  	}
    71  	if len(results.Results) != 1 {
    72  		return status.History{}, errors.Errorf("expected 1 result got %d", len(results.Results))
    73  	}
    74  	if results.Results[0].Error != nil {
    75  		return status.History{}, errors.Annotatef(results.Results[0].Error, "while processing the request")
    76  	}
    77  	history := make(status.History, len(results.Results[0].History.Statuses))
    78  	if results.Results[0].History.Error != nil {
    79  		return status.History{}, results.Results[0].History.Error
    80  	}
    81  	for i, h := range results.Results[0].History.Statuses {
    82  		history[i] = status.DetailedStatus{
    83  			Status:  status.Status(h.Status),
    84  			Info:    h.Info,
    85  			Data:    h.Data,
    86  			Since:   h.Since,
    87  			Kind:    status.HistoryKind(h.Kind),
    88  			Version: h.Version,
    89  			// TODO(perrito666) make sure these are still used.
    90  			Life: h.Life,
    91  			Err:  h.Err,
    92  		}
    93  		// TODO(perrito666) https://launchpad.net/bugs/1577589
    94  		if !history[i].Kind.Valid() {
    95  			logger.Errorf("history returned an unknown status kind %q", h.Kind)
    96  		}
    97  	}
    98  	return history, nil
    99  }
   100  
   101  // Resolved clears errors on a unit.
   102  func (c *Client) Resolved(unit string, retry bool) error {
   103  	p := params.Resolved{
   104  		UnitName: unit,
   105  		Retry:    retry,
   106  	}
   107  	return c.facade.FacadeCall("Resolved", p, nil)
   108  }
   109  
   110  // RetryProvisioning updates the provisioning status of a machine allowing the
   111  // provisioner to retry.
   112  func (c *Client) RetryProvisioning(machines ...names.MachineTag) ([]params.ErrorResult, error) {
   113  	p := params.Entities{}
   114  	p.Entities = make([]params.Entity, len(machines))
   115  	for i, machine := range machines {
   116  		p.Entities[i] = params.Entity{Tag: machine.String()}
   117  	}
   118  	var results params.ErrorResults
   119  	err := c.facade.FacadeCall("RetryProvisioning", p, &results)
   120  	return results.Results, err
   121  }
   122  
   123  // PublicAddress returns the public address of the specified
   124  // machine or unit. For a machine, target is an id not a tag.
   125  func (c *Client) PublicAddress(target string) (string, error) {
   126  	var results params.PublicAddressResults
   127  	p := params.PublicAddress{Target: target}
   128  	err := c.facade.FacadeCall("PublicAddress", p, &results)
   129  	return results.PublicAddress, err
   130  }
   131  
   132  // PrivateAddress returns the private address of the specified
   133  // machine or unit.
   134  func (c *Client) PrivateAddress(target string) (string, error) {
   135  	var results params.PrivateAddressResults
   136  	p := params.PrivateAddress{Target: target}
   137  	err := c.facade.FacadeCall("PrivateAddress", p, &results)
   138  	return results.PrivateAddress, err
   139  }
   140  
   141  // AddMachines adds new machines with the supplied parameters.
   142  func (c *Client) AddMachines(machineParams []params.AddMachineParams) ([]params.AddMachinesResult, error) {
   143  	args := params.AddMachines{
   144  		MachineParams: machineParams,
   145  	}
   146  	results := new(params.AddMachinesResults)
   147  	err := c.facade.FacadeCall("AddMachinesV2", args, results)
   148  	return results.Machines, err
   149  }
   150  
   151  // ProvisioningScript returns a shell script that, when run,
   152  // provisions a machine agent on the machine executing the script.
   153  func (c *Client) ProvisioningScript(args params.ProvisioningScriptParams) (script string, err error) {
   154  	var result params.ProvisioningScriptResult
   155  	if err = c.facade.FacadeCall("ProvisioningScript", args, &result); err != nil {
   156  		return "", err
   157  	}
   158  	return result.Script, nil
   159  }
   160  
   161  // DestroyMachines removes a given set of machines.
   162  func (c *Client) DestroyMachines(machines ...string) error {
   163  	params := params.DestroyMachines{MachineNames: machines}
   164  	return c.facade.FacadeCall("DestroyMachines", params, nil)
   165  }
   166  
   167  // ForceDestroyMachines removes a given set of machines and all associated units.
   168  func (c *Client) ForceDestroyMachines(machines ...string) error {
   169  	params := params.DestroyMachines{Force: true, MachineNames: machines}
   170  	return c.facade.FacadeCall("DestroyMachines", params, nil)
   171  }
   172  
   173  // GetModelConstraints returns the constraints for the model.
   174  func (c *Client) GetModelConstraints() (constraints.Value, error) {
   175  	results := new(params.GetConstraintsResults)
   176  	err := c.facade.FacadeCall("GetModelConstraints", nil, results)
   177  	return results.Constraints, err
   178  }
   179  
   180  // SetModelConstraints specifies the constraints for the model.
   181  func (c *Client) SetModelConstraints(constraints constraints.Value) error {
   182  	params := params.SetConstraints{
   183  		Constraints: constraints,
   184  	}
   185  	return c.facade.FacadeCall("SetModelConstraints", params, nil)
   186  }
   187  
   188  // ModelUUID returns the model UUID from the client connection
   189  // and reports whether it is valud.
   190  func (c *Client) ModelUUID() (string, bool) {
   191  	tag, ok := c.st.ModelTag()
   192  	if !ok {
   193  		return "", false
   194  	}
   195  	return tag.Id(), true
   196  }
   197  
   198  // ModelUserInfo returns information on all users in the model.
   199  func (c *Client) ModelUserInfo() ([]params.ModelUserInfo, error) {
   200  	var results params.ModelUserInfoResults
   201  	err := c.facade.FacadeCall("ModelUserInfo", nil, &results)
   202  	if err != nil {
   203  		return nil, errors.Trace(err)
   204  	}
   205  
   206  	info := []params.ModelUserInfo{}
   207  	for i, result := range results.Results {
   208  		if result.Result == nil {
   209  			return nil, errors.Errorf("unexpected nil result at position %d", i)
   210  		}
   211  		info = append(info, *result.Result)
   212  	}
   213  	return info, nil
   214  }
   215  
   216  // WatchAll returns an AllWatcher, from which you can request the Next
   217  // collection of Deltas.
   218  func (c *Client) WatchAll() (*AllWatcher, error) {
   219  	var info params.AllWatcherId
   220  	if err := c.facade.FacadeCall("WatchAll", nil, &info); err != nil {
   221  		return nil, err
   222  	}
   223  	return NewAllWatcher(c.st, &info.AllWatcherId), nil
   224  }
   225  
   226  // Close closes the Client's underlying State connection
   227  // Client is unique among the api.State facades in closing its own State
   228  // connection, but it is conventional to use a Client object without any access
   229  // to its underlying state connection.
   230  func (c *Client) Close() error {
   231  	return c.st.Close()
   232  }
   233  
   234  // SetModelAgentVersion sets the model agent-version setting
   235  // to the given value.
   236  func (c *Client) SetModelAgentVersion(version version.Number) error {
   237  	args := params.SetModelAgentVersion{Version: version}
   238  	return c.facade.FacadeCall("SetModelAgentVersion", args, nil)
   239  }
   240  
   241  // AbortCurrentUpgrade aborts and archives the current upgrade
   242  // synchronisation record, if any.
   243  func (c *Client) AbortCurrentUpgrade() error {
   244  	return c.facade.FacadeCall("AbortCurrentUpgrade", nil, nil)
   245  }
   246  
   247  // FindTools returns a List containing all tools matching the specified parameters.
   248  func (c *Client) FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) {
   249  	args := params.FindToolsParams{
   250  		MajorVersion: majorVersion,
   251  		MinorVersion: minorVersion,
   252  		Arch:         arch,
   253  		Series:       series,
   254  	}
   255  	err = c.facade.FacadeCall("FindTools", args, &result)
   256  	return result, err
   257  }
   258  
   259  // AddLocalCharm prepares the given charm with a local: schema in its
   260  // URL, and uploads it via the API server, returning the assigned
   261  // charm URL.
   262  func (c *Client) AddLocalCharm(curl *charm.URL, ch charm.Charm) (*charm.URL, error) {
   263  	if curl.Schema != "local" {
   264  		return nil, errors.Errorf("expected charm URL with local: schema, got %q", curl.String())
   265  	}
   266  
   267  	if err := c.validateCharmVersion(ch); err != nil {
   268  		return nil, errors.Trace(err)
   269  	}
   270  
   271  	// Package the charm for uploading.
   272  	var archive *os.File
   273  	switch ch := ch.(type) {
   274  	case *charm.CharmDir:
   275  		var err error
   276  		if archive, err = ioutil.TempFile("", "charm"); err != nil {
   277  			return nil, errors.Annotate(err, "cannot create temp file")
   278  		}
   279  		defer os.Remove(archive.Name())
   280  		defer archive.Close()
   281  		if err := ch.ArchiveTo(archive); err != nil {
   282  			return nil, errors.Annotate(err, "cannot repackage charm")
   283  		}
   284  		if _, err := archive.Seek(0, 0); err != nil {
   285  			return nil, errors.Annotate(err, "cannot rewind packaged charm")
   286  		}
   287  	case *charm.CharmArchive:
   288  		var err error
   289  		if archive, err = os.Open(ch.Path); err != nil {
   290  			return nil, errors.Annotate(err, "cannot read charm archive")
   291  		}
   292  		defer archive.Close()
   293  	default:
   294  		return nil, errors.Errorf("unknown charm type %T", ch)
   295  	}
   296  
   297  	curl, err := c.UploadCharm(curl, archive)
   298  	if err != nil {
   299  		return nil, errors.Trace(err)
   300  	}
   301  	return curl, nil
   302  }
   303  
   304  // UploadCharm sends the content to the API server using an HTTP post.
   305  func (c *Client) UploadCharm(curl *charm.URL, content io.ReadSeeker) (*charm.URL, error) {
   306  	args := url.Values{}
   307  	args.Add("series", curl.Series)
   308  	args.Add("schema", curl.Schema)
   309  	args.Add("revision", strconv.Itoa(curl.Revision))
   310  	apiURI := url.URL{Path: "/charms", RawQuery: args.Encode()}
   311  
   312  	contentType := "application/zip"
   313  	var resp params.CharmsResponse
   314  	if err := c.httpPost(content, apiURI.String(), contentType, &resp); err != nil {
   315  		return nil, errors.Trace(err)
   316  	}
   317  
   318  	curl, err := charm.ParseURL(resp.CharmURL)
   319  	if err != nil {
   320  		return nil, errors.Annotatef(err, "bad charm URL in response")
   321  	}
   322  	return curl, nil
   323  }
   324  
   325  type minJujuVersionErr struct {
   326  	*errors.Err
   327  }
   328  
   329  func minVersionError(minver, jujuver version.Number) error {
   330  	err := errors.NewErr("charm's min version (%s) is higher than this juju environment's version (%s)",
   331  		minver, jujuver)
   332  	err.SetLocation(1)
   333  	return minJujuVersionErr{&err}
   334  }
   335  
   336  func (c *Client) validateCharmVersion(ch charm.Charm) error {
   337  	minver := ch.Meta().MinJujuVersion
   338  	if minver != version.Zero {
   339  		agentver, err := c.AgentVersion()
   340  		if err != nil {
   341  			return errors.Trace(err)
   342  		}
   343  
   344  		if minver.Compare(agentver) > 0 {
   345  			return minVersionError(minver, agentver)
   346  		}
   347  	}
   348  	return nil
   349  }
   350  
   351  // TODO(ericsnow) Use charmstore.CharmID for AddCharm() & AddCharmWithAuth().
   352  
   353  // AddCharm adds the given charm URL (which must include revision) to
   354  // the model, if it does not exist yet. Local charms are not
   355  // supported, only charm store URLs. See also AddLocalCharm() in the
   356  // client-side API.
   357  //
   358  // If the AddCharm API call fails because of an authorization error
   359  // when retrieving the charm from the charm store, an error
   360  // satisfying params.IsCodeUnauthorized will be returned.
   361  func (c *Client) AddCharm(curl *charm.URL, channel csparams.Channel) error {
   362  	args := params.AddCharm{
   363  		URL:     curl.String(),
   364  		Channel: string(channel),
   365  	}
   366  	if err := c.facade.FacadeCall("AddCharm", args, nil); err != nil {
   367  		return errors.Trace(err)
   368  	}
   369  	return nil
   370  }
   371  
   372  // AddCharmWithAuthorization is like AddCharm except it also provides
   373  // the given charmstore macaroon for the juju server to use when
   374  // obtaining the charm from the charm store. The macaroon is
   375  // conventionally obtained from the /delegatable-macaroon endpoint in
   376  // the charm store.
   377  //
   378  // If the AddCharmWithAuthorization API call fails because of an
   379  // authorization error when retrieving the charm from the charm store,
   380  // an error satisfying params.IsCodeUnauthorized will be returned.
   381  func (c *Client) AddCharmWithAuthorization(curl *charm.URL, channel csparams.Channel, csMac *macaroon.Macaroon) error {
   382  	args := params.AddCharmWithAuthorization{
   383  		URL:                curl.String(),
   384  		Channel:            string(channel),
   385  		CharmStoreMacaroon: csMac,
   386  	}
   387  	if err := c.facade.FacadeCall("AddCharmWithAuthorization", args, nil); err != nil {
   388  		return errors.Trace(err)
   389  	}
   390  	return nil
   391  }
   392  
   393  // ResolveCharm resolves the best available charm URLs with series, for charm
   394  // locations without a series specified.
   395  func (c *Client) ResolveCharm(ref *charm.URL) (*charm.URL, error) {
   396  	args := params.ResolveCharms{References: []string{ref.String()}}
   397  	result := new(params.ResolveCharmResults)
   398  	if err := c.facade.FacadeCall("ResolveCharms", args, result); err != nil {
   399  		return nil, err
   400  	}
   401  	if len(result.URLs) == 0 {
   402  		return nil, errors.New("unexpected empty response")
   403  	}
   404  	urlInfo := result.URLs[0]
   405  	if urlInfo.Error != "" {
   406  		return nil, errors.New(urlInfo.Error)
   407  	}
   408  	url, err := charm.ParseURL(urlInfo.URL)
   409  	if err != nil {
   410  		return nil, errors.Trace(err)
   411  	}
   412  	return url, nil
   413  }
   414  
   415  // OpenCharm streams out the identified charm from the controller via
   416  // the API.
   417  func (c *Client) OpenCharm(curl *charm.URL) (io.ReadCloser, error) {
   418  	query := make(url.Values)
   419  	query.Add("url", curl.String())
   420  	query.Add("file", "*")
   421  	return c.OpenURI("/charms", query)
   422  }
   423  
   424  // OpenURI performs a GET on a Juju HTTP endpoint returning the
   425  func (c *Client) OpenURI(uri string, query url.Values) (io.ReadCloser, error) {
   426  	// The returned httpClient sets the base url to /model/<uuid> if it can.
   427  	httpClient, err := c.st.HTTPClient()
   428  	if err != nil {
   429  		return nil, errors.Trace(err)
   430  	}
   431  	blob, err := openBlob(httpClient, uri, query)
   432  	if err != nil {
   433  		return nil, errors.Trace(err)
   434  	}
   435  	return blob, nil
   436  }
   437  
   438  // NewCharmDownloader returns a new charm downloader that wraps the
   439  // provided API client.
   440  func NewCharmDownloader(client *Client) *downloader.Downloader {
   441  	dlr := &downloader.Downloader{
   442  		OpenBlob: func(url *url.URL) (io.ReadCloser, error) {
   443  			curl, err := charm.ParseURL(url.String())
   444  			if err != nil {
   445  				return nil, errors.Annotate(err, "did not receive a valid charm URL")
   446  			}
   447  			reader, err := client.OpenCharm(curl)
   448  			if err != nil {
   449  				return nil, errors.Trace(err)
   450  			}
   451  			return reader, nil
   452  		},
   453  	}
   454  	return dlr
   455  }
   456  
   457  // UploadTools uploads tools at the specified location to the API server over HTTPS.
   458  func (c *Client) UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (tools.List, error) {
   459  	endpoint := fmt.Sprintf("/tools?binaryVersion=%s&series=%s", vers, strings.Join(additionalSeries, ","))
   460  	contentType := "application/x-tar-gz"
   461  	var resp params.ToolsResult
   462  	if err := c.httpPost(r, endpoint, contentType, &resp); err != nil {
   463  		return nil, errors.Trace(err)
   464  	}
   465  	return resp.ToolsList, nil
   466  }
   467  
   468  func (c *Client) httpPost(content io.ReadSeeker, endpoint, contentType string, response interface{}) error {
   469  	req, err := http.NewRequest("POST", endpoint, nil)
   470  	if err != nil {
   471  		return errors.Annotate(err, "cannot create upload request")
   472  	}
   473  	req.Header.Set("Content-Type", contentType)
   474  
   475  	// The returned httpClient sets the base url to /model/<uuid> if it can.
   476  	httpClient, err := c.st.HTTPClient()
   477  	if err != nil {
   478  		return errors.Trace(err)
   479  	}
   480  
   481  	if err := httpClient.Do(req, content, response); err != nil {
   482  		return errors.Trace(err)
   483  	}
   484  	return nil
   485  }
   486  
   487  // APIHostPorts returns a slice of network.HostPort for each API server.
   488  func (c *Client) APIHostPorts() ([][]network.HostPort, error) {
   489  	var result params.APIHostPortsResult
   490  	if err := c.facade.FacadeCall("APIHostPorts", nil, &result); err != nil {
   491  		return nil, err
   492  	}
   493  	return result.NetworkHostsPorts(), nil
   494  }
   495  
   496  // AgentVersion reports the version number of the api server.
   497  func (c *Client) AgentVersion() (version.Number, error) {
   498  	var result params.AgentVersionResult
   499  	if err := c.facade.FacadeCall("AgentVersion", nil, &result); err != nil {
   500  		return version.Number{}, err
   501  	}
   502  	return result.Version, nil
   503  }
   504  
   505  // websocketDialConfig is called instead of websocket.DialConfig so we can
   506  // override it in tests.
   507  var websocketDialConfig = func(config *websocket.Config) (base.Stream, error) {
   508  	c, err := websocket.DialConfig(config)
   509  	if err != nil {
   510  		return nil, errors.Trace(err)
   511  	}
   512  	return websocketStream{c}, nil
   513  }
   514  
   515  type websocketStream struct {
   516  	*websocket.Conn
   517  }
   518  
   519  func (c websocketStream) ReadJSON(v interface{}) error {
   520  	return websocket.JSON.Receive(c.Conn, v)
   521  }
   522  
   523  func (c websocketStream) WriteJSON(v interface{}) error {
   524  	return websocket.JSON.Send(c.Conn, v)
   525  }
   526  
   527  // TODO(ericsnow) Fold DebugLogParams into params.LogStreamConfig.
   528  
   529  // DebugLogParams holds parameters for WatchDebugLog that control the
   530  // filtering of the log messages. If the structure is zero initialized, the
   531  // entire log file is sent back starting from the end, and until the user
   532  // closes the connection.
   533  type DebugLogParams struct {
   534  	// IncludeEntity lists entity tags to include in the response. Tags may
   535  	// finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2. If
   536  	// none are set, then all lines are considered included.
   537  	IncludeEntity []string
   538  	// IncludeModule lists logging modules to include in the response. If none
   539  	// are set all modules are considered included.  If a module is specified,
   540  	// all the submodules also match.
   541  	IncludeModule []string
   542  	// ExcludeEntity lists entity tags to exclude from the response. As with
   543  	// IncludeEntity the values may finish with a '*'.
   544  	ExcludeEntity []string
   545  	// ExcludeModule lists logging modules to exclude from the resposne. If a
   546  	// module is specified, all the submodules are also excluded.
   547  	ExcludeModule []string
   548  	// Limit defines the maximum number of lines to return. Once this many
   549  	// have been sent, the socket is closed.  If zero, all filtered lines are
   550  	// sent down the connection until the client closes the connection.
   551  	Limit uint
   552  	// Backlog tells the server to try to go back this many lines before
   553  	// starting filtering. If backlog is zero and replay is false, then there
   554  	// may be an initial delay until the next matching log message is written.
   555  	Backlog uint
   556  	// Level specifies the minimum logging level to be sent back in the response.
   557  	Level loggo.Level
   558  	// Replay tells the server to start at the start of the log file rather
   559  	// than the end. If replay is true, backlog is ignored.
   560  	Replay bool
   561  	// NoTail tells the server to only return the logs it has now, and not
   562  	// to wait for new logs to arrive.
   563  	NoTail bool
   564  }
   565  
   566  func (args DebugLogParams) URLQuery() url.Values {
   567  	attrs := url.Values{
   568  		"includeEntity": args.IncludeEntity,
   569  		"includeModule": args.IncludeModule,
   570  		"excludeEntity": args.ExcludeEntity,
   571  		"excludeModule": args.ExcludeModule,
   572  	}
   573  	if args.Replay {
   574  		attrs.Set("replay", fmt.Sprint(args.Replay))
   575  	}
   576  	if args.NoTail {
   577  		attrs.Set("noTail", fmt.Sprint(args.NoTail))
   578  	}
   579  	if args.Limit > 0 {
   580  		attrs.Set("maxLines", fmt.Sprint(args.Limit))
   581  	}
   582  	if args.Backlog > 0 {
   583  		attrs.Set("backlog", fmt.Sprint(args.Backlog))
   584  	}
   585  	if args.Level != loggo.UNSPECIFIED {
   586  		attrs.Set("level", fmt.Sprint(args.Level))
   587  	}
   588  	return attrs
   589  }
   590  
   591  // LogMessage is a structured logging entry.
   592  type LogMessage struct {
   593  	Entity    string
   594  	Timestamp time.Time
   595  	Severity  string
   596  	Module    string
   597  	Location  string
   598  	Message   string
   599  }
   600  
   601  // WatchDebugLog returns a channel of structured Log Messages. Only log entries
   602  // that match the filtering specified in the DebugLogParams are returned.
   603  func (c *Client) WatchDebugLog(args DebugLogParams) (<-chan LogMessage, error) {
   604  	// Prepare URL query attributes.
   605  	attrs := args.URLQuery()
   606  
   607  	connection, err := c.st.ConnectStream("/log", attrs)
   608  	if err != nil {
   609  		return nil, errors.Trace(err)
   610  	}
   611  
   612  	messages := make(chan LogMessage)
   613  	go func() {
   614  		defer close(messages)
   615  
   616  		for {
   617  			var msg params.LogMessage
   618  			err := connection.ReadJSON(&msg)
   619  			if err != nil {
   620  				return
   621  			}
   622  			messages <- LogMessage{
   623  				Entity:    msg.Entity,
   624  				Timestamp: msg.Timestamp,
   625  				Severity:  msg.Severity,
   626  				Module:    msg.Module,
   627  				Location:  msg.Location,
   628  				Message:   msg.Message,
   629  			}
   630  		}
   631  	}()
   632  
   633  	return messages, nil
   634  }