package gcplogs // import "github.com/docker/docker/daemon/logger/gcplogs" import ( "context" "fmt" "sync" "sync/atomic" "time" "github.com/docker/docker/daemon/logger" "cloud.google.com/go/compute/metadata" "cloud.google.com/go/logging" "github.com/sirupsen/logrus" mrpb "google.golang.org/genproto/googleapis/api/monitoredres" ) const ( name = "gcplogs" projectOptKey = "gcp-project" logLabelsKey = "labels" logLabelsRegexKey = "labels-regex" logEnvKey = "env" logEnvRegexKey = "env-regex" logCmdKey = "gcp-log-cmd" logZoneKey = "gcp-meta-zone" logNameKey = "gcp-meta-name" logIDKey = "gcp-meta-id" ) var ( // The number of logs the gcplogs driver has dropped. droppedLogs uint64 onGCE bool // instance metadata populated from the metadata server if available projectID string zone string instanceName string instanceID string ) func init() { if err := logger.RegisterLogDriver(name, New); err != nil { logrus.Fatal(err) } if err := logger.RegisterLogOptValidator(name, ValidateLogOpts); err != nil { logrus.Fatal(err) } } type gcplogs struct { client *logging.Client logger *logging.Logger instance *instanceInfo container *containerInfo } type dockerLogEntry struct { Instance *instanceInfo `json:"instance,omitempty"` Container *containerInfo `json:"container,omitempty"` Message string `json:"message,omitempty"` } type instanceInfo struct { Zone string `json:"zone,omitempty"` Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` } type containerInfo struct { Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` ImageName string `json:"imageName,omitempty"` ImageID string `json:"imageId,omitempty"` Created time.Time `json:"created,omitempty"` Command string `json:"command,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } var initGCPOnce sync.Once func initGCP() { initGCPOnce.Do(func() { onGCE = metadata.OnGCE() if onGCE { // These will fail on instances if the metadata service is // down or the client is compiled with an API version that // has been removed. Since these are not vital, let's ignore // them and make their fields in the dockerLogEntry ,omitempty projectID, _ = metadata.ProjectID() zone, _ = metadata.Zone() instanceName, _ = metadata.InstanceName() instanceID, _ = metadata.InstanceID() } }) } // New creates a new logger that logs to Google Cloud Logging using the application // default credentials. // // See https://developers.google.com/identity/protocols/application-default-credentials func New(info logger.Info) (logger.Logger, error) { initGCP() var project string if projectID != "" { project = projectID } if projectID, found := info.Config[projectOptKey]; found { project = projectID } if project == "" { return nil, fmt.Errorf("No project was specified and couldn't read project from the metadata server. Please specify a project") } // Issue #29344: gcplogs segfaults (static binary) // If HOME is not set, logging.NewClient() will call os/user.Current() via oauth2/google. // However, in static binary, os/user.Current() leads to segfault due to a glibc issue that won't be fixed // in a short term. (golang/go#13470, https://sourceware.org/bugzilla/show_bug.cgi?id=19341) // So we forcibly set HOME so as to avoid call to os/user/Current() if err := ensureHomeIfIAmStatic(); err != nil { return nil, err } c, err := logging.NewClient(context.Background(), project) if err != nil { return nil, err } var instanceResource *instanceInfo if onGCE { instanceResource = &instanceInfo{ Zone: zone, Name: instanceName, ID: instanceID, } } else if info.Config[logZoneKey] != "" || info.Config[logNameKey] != "" || info.Config[logIDKey] != "" { instanceResource = &instanceInfo{ Zone: info.Config[logZoneKey], Name: info.Config[logNameKey], ID: info.Config[logIDKey], } } options := []logging.LoggerOption{} if instanceResource != nil { vmMrpb := logging.CommonResource( &mrpb.MonitoredResource{ Type: "gce_instance", Labels: map[string]string{ "instance_id": instanceResource.ID, "zone": instanceResource.Zone, }, }, ) options = []logging.LoggerOption{vmMrpb} } lg := c.Logger("gcplogs-docker-driver", options...) if err := c.Ping(context.Background()); err != nil { return nil, fmt.Errorf("unable to connect or authenticate with Google Cloud Logging: %v", err) } extraAttributes, err := info.ExtraAttributes(nil) if err != nil { return nil, err } l := &gcplogs{ client: c, logger: lg, container: &containerInfo{ Name: info.ContainerName, ID: info.ContainerID, ImageName: info.ContainerImageName, ImageID: info.ContainerImageID, Created: info.ContainerCreated, Metadata: extraAttributes, }, } if info.Config[logCmdKey] == "true" { l.container.Command = info.Command() } if instanceResource != nil { l.instance = instanceResource } // The logger "overflows" at a rate of 10,000 logs per second and this // overflow func is called. We want to surface the error to the user // without overly spamming /var/log/docker.log so we log the first time // we overflow and every 1000th time after. c.OnError = func(err error) { if err == logging.ErrOverflow { if i := atomic.AddUint64(&droppedLogs, 1); i%1000 == 1 { logrus.Errorf("gcplogs driver has dropped %v logs", i) } } else { logrus.Error(err) } } return l, nil } // ValidateLogOpts validates the opts passed to the gcplogs driver. Currently, the gcplogs // driver doesn't take any arguments. func ValidateLogOpts(cfg map[string]string) error { for k := range cfg { switch k { case projectOptKey, logLabelsKey, logLabelsRegexKey, logEnvKey, logEnvRegexKey, logCmdKey, logZoneKey, logNameKey, logIDKey: default: return fmt.Errorf("%q is not a valid option for the gcplogs driver", k) } } return nil } func (l *gcplogs) Log(m *logger.Message) error { message := string(m.Line) ts := m.Timestamp logger.PutMessage(m) l.logger.Log(logging.Entry{ Timestamp: ts, Payload: &dockerLogEntry{ Instance: l.instance, Container: l.container, Message: message, }, }) return nil } func (l *gcplogs) Close() error { l.logger.Flush() return l.client.Close() } func (l *gcplogs) Name() string { return name }