gcplogging.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. package gcplogs // import "github.com/docker/docker/daemon/logger/gcplogs"
  2. import (
  3. "context"
  4. "fmt"
  5. "sync"
  6. "sync/atomic"
  7. "time"
  8. "github.com/docker/docker/daemon/logger"
  9. "cloud.google.com/go/compute/metadata"
  10. "cloud.google.com/go/logging"
  11. "github.com/containerd/log"
  12. mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
  13. )
  14. const (
  15. name = "gcplogs"
  16. projectOptKey = "gcp-project"
  17. logLabelsKey = "labels"
  18. logLabelsRegexKey = "labels-regex"
  19. logEnvKey = "env"
  20. logEnvRegexKey = "env-regex"
  21. logCmdKey = "gcp-log-cmd"
  22. logZoneKey = "gcp-meta-zone"
  23. logNameKey = "gcp-meta-name"
  24. logIDKey = "gcp-meta-id"
  25. )
  26. var (
  27. // The number of logs the gcplogs driver has dropped.
  28. droppedLogs uint64
  29. onGCE bool
  30. // instance metadata populated from the metadata server if available
  31. projectID string
  32. zone string
  33. instanceName string
  34. instanceID string
  35. )
  36. func init() {
  37. if err := logger.RegisterLogDriver(name, New); err != nil {
  38. panic(err)
  39. }
  40. if err := logger.RegisterLogOptValidator(name, ValidateLogOpts); err != nil {
  41. panic(err)
  42. }
  43. }
  44. type gcplogs struct {
  45. client *logging.Client
  46. logger *logging.Logger
  47. instance *instanceInfo
  48. container *containerInfo
  49. }
  50. type dockerLogEntry struct {
  51. Instance *instanceInfo `json:"instance,omitempty"`
  52. Container *containerInfo `json:"container,omitempty"`
  53. Message string `json:"message,omitempty"`
  54. }
  55. type instanceInfo struct {
  56. Zone string `json:"zone,omitempty"`
  57. Name string `json:"name,omitempty"`
  58. ID string `json:"id,omitempty"`
  59. }
  60. type containerInfo struct {
  61. Name string `json:"name,omitempty"`
  62. ID string `json:"id,omitempty"`
  63. ImageName string `json:"imageName,omitempty"`
  64. ImageID string `json:"imageId,omitempty"`
  65. Created time.Time `json:"created,omitempty"`
  66. Command string `json:"command,omitempty"`
  67. Metadata map[string]string `json:"metadata,omitempty"`
  68. }
  69. var initGCPOnce sync.Once
  70. func initGCP() {
  71. initGCPOnce.Do(func() {
  72. onGCE = metadata.OnGCE()
  73. if onGCE {
  74. // These will fail on instances if the metadata service is
  75. // down or the client is compiled with an API version that
  76. // has been removed. Since these are not vital, let's ignore
  77. // them and make their fields in the dockerLogEntry ,omitempty
  78. projectID, _ = metadata.ProjectID()
  79. zone, _ = metadata.Zone()
  80. instanceName, _ = metadata.InstanceName()
  81. instanceID, _ = metadata.InstanceID()
  82. }
  83. })
  84. }
  85. // New creates a new logger that logs to Google Cloud Logging using the application
  86. // default credentials.
  87. //
  88. // See https://developers.google.com/identity/protocols/application-default-credentials
  89. func New(info logger.Info) (logger.Logger, error) {
  90. initGCP()
  91. var project string
  92. if projectID != "" {
  93. project = projectID
  94. }
  95. if projectID, found := info.Config[projectOptKey]; found {
  96. project = projectID
  97. }
  98. if project == "" {
  99. return nil, fmt.Errorf("No project was specified and couldn't read project from the metadata server. Please specify a project")
  100. }
  101. c, err := logging.NewClient(context.Background(), project)
  102. if err != nil {
  103. return nil, err
  104. }
  105. var instanceResource *instanceInfo
  106. if onGCE {
  107. instanceResource = &instanceInfo{
  108. Zone: zone,
  109. Name: instanceName,
  110. ID: instanceID,
  111. }
  112. } else if info.Config[logZoneKey] != "" || info.Config[logNameKey] != "" || info.Config[logIDKey] != "" {
  113. instanceResource = &instanceInfo{
  114. Zone: info.Config[logZoneKey],
  115. Name: info.Config[logNameKey],
  116. ID: info.Config[logIDKey],
  117. }
  118. }
  119. options := []logging.LoggerOption{}
  120. if instanceResource != nil {
  121. vmMrpb := logging.CommonResource(
  122. &mrpb.MonitoredResource{
  123. Type: "gce_instance",
  124. Labels: map[string]string{
  125. "instance_id": instanceResource.ID,
  126. "zone": instanceResource.Zone,
  127. },
  128. },
  129. )
  130. options = []logging.LoggerOption{vmMrpb}
  131. }
  132. lg := c.Logger("gcplogs-docker-driver", options...)
  133. if err := c.Ping(context.Background()); err != nil {
  134. return nil, fmt.Errorf("unable to connect or authenticate with Google Cloud Logging: %v", err)
  135. }
  136. extraAttributes, err := info.ExtraAttributes(nil)
  137. if err != nil {
  138. return nil, err
  139. }
  140. l := &gcplogs{
  141. client: c,
  142. logger: lg,
  143. container: &containerInfo{
  144. Name: info.ContainerName,
  145. ID: info.ContainerID,
  146. ImageName: info.ContainerImageName,
  147. ImageID: info.ContainerImageID,
  148. Created: info.ContainerCreated,
  149. Metadata: extraAttributes,
  150. },
  151. }
  152. if info.Config[logCmdKey] == "true" {
  153. l.container.Command = info.Command()
  154. }
  155. if instanceResource != nil {
  156. l.instance = instanceResource
  157. }
  158. // The logger "overflows" at a rate of 10,000 logs per second and this
  159. // overflow func is called. We want to surface the error to the user
  160. // without overly spamming /var/log/docker.log so we log the first time
  161. // we overflow and every 1000th time after.
  162. c.OnError = func(err error) {
  163. if err == logging.ErrOverflow {
  164. if i := atomic.AddUint64(&droppedLogs, 1); i%1000 == 1 {
  165. log.G(context.TODO()).Errorf("gcplogs driver has dropped %v logs", i)
  166. }
  167. } else {
  168. log.G(context.TODO()).Error(err)
  169. }
  170. }
  171. return l, nil
  172. }
  173. // ValidateLogOpts validates the opts passed to the gcplogs driver. Currently, the gcplogs
  174. // driver doesn't take any arguments.
  175. func ValidateLogOpts(cfg map[string]string) error {
  176. for k := range cfg {
  177. switch k {
  178. case projectOptKey, logLabelsKey, logLabelsRegexKey, logEnvKey, logEnvRegexKey, logCmdKey, logZoneKey, logNameKey, logIDKey:
  179. default:
  180. return fmt.Errorf("%q is not a valid option for the gcplogs driver", k)
  181. }
  182. }
  183. return nil
  184. }
  185. func (l *gcplogs) Log(m *logger.Message) error {
  186. message := string(m.Line)
  187. ts := m.Timestamp
  188. logger.PutMessage(m)
  189. l.logger.Log(logging.Entry{
  190. Timestamp: ts,
  191. Payload: &dockerLogEntry{
  192. Instance: l.instance,
  193. Container: l.container,
  194. Message: message,
  195. },
  196. })
  197. return nil
  198. }
  199. func (l *gcplogs) Close() error {
  200. l.logger.Flush()
  201. return l.client.Close()
  202. }
  203. func (l *gcplogs) Name() string {
  204. return name
  205. }