github.com/google/cloudprober@v0.11.3/surfacers/pubsub/pubsub.go (about) 1 // Copyright 2020 The Cloudprober Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package pubsub implements the "pubsub" surfacer. This surfacer type is in 16 // experimental phase right now. 17 package pubsub 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 "sync" 24 "time" 25 26 "cloud.google.com/go/compute/metadata" 27 "cloud.google.com/go/pubsub" 28 "github.com/google/cloudprober/logger" 29 "github.com/google/cloudprober/metrics" 30 "github.com/google/cloudprober/surfacers/common/compress" 31 "github.com/google/cloudprober/surfacers/common/options" 32 "github.com/google/cloudprober/sysvars" 33 34 configpb "github.com/google/cloudprober/surfacers/pubsub/proto" 35 ) 36 37 const ( 38 publishTimeout = 10 * time.Second 39 compressedAttr = "compressed" 40 starttimeAttr = "starttime" 41 ) 42 43 // IsCompressed takes message attribute map and returns true if compressed 44 // attribute is set to true. 45 func IsCompressed(attr map[string]string) bool { 46 return attr[compressedAttr] == "true" 47 } 48 49 // StartTime takes message attributes map and returns the value of the 50 // starttime attribute. 51 func StartTime(attr map[string]string) string { 52 return attr[starttimeAttr] 53 } 54 55 var newPubsubClient = func(ctx context.Context, project string) (*pubsub.Client, error) { 56 return pubsub.NewClient(ctx, project) 57 } 58 59 // Surfacer implements a pubsub surfacer. 60 type Surfacer struct { 61 // Configuration 62 c *configpb.SurfacerConf 63 opts *options.Options 64 65 // Channel for incoming data. 66 inChan chan *metrics.EventMetrics 67 publishResultChan chan *pubsub.PublishResult 68 69 topic *pubsub.Topic 70 topicName string 71 gcpProject string 72 73 l *logger.Logger 74 starttime string 75 compressionBuffer *compress.CompressionBuffer 76 processInputWg sync.WaitGroup 77 } 78 79 func (s *Surfacer) publishMessage(globalCtx context.Context, data []byte) { 80 boolToString := map[bool]string{ 81 true: "true", 82 false: "false", 83 } 84 msg := &pubsub.Message{ 85 Attributes: map[string]string{ 86 compressedAttr: boolToString[s.c.GetCompressionEnabled()], 87 starttimeAttr: s.starttime, 88 }, 89 Data: data, 90 } 91 92 publishCtx, cancel := context.WithTimeout(globalCtx, publishTimeout) 93 defer cancel() 94 s.publishResultChan <- s.topic.Publish(publishCtx, msg) 95 } 96 97 func (s *Surfacer) processInput(ctx context.Context) { 98 defer s.processInputWg.Done() 99 100 for { 101 select { 102 case <-ctx.Done(): 103 return 104 // Publish the EventMetrics to the topic as a pubsub message. 105 case em, ok := <-s.inChan: 106 if !ok { 107 return 108 } 109 if s.c.GetCompressionEnabled() { 110 s.compressionBuffer.WriteLineToBuffer(em.String()) 111 } else { 112 s.publishMessage(ctx, []byte(em.String())) 113 } 114 } 115 } 116 } 117 118 func (s *Surfacer) init(ctx context.Context) error { 119 s.inChan = make(chan *metrics.EventMetrics, s.opts.MetricsBufferSize) 120 121 // We use start timestamp in millisecond as the incarnation id. 122 s.starttime = strconv.FormatInt(time.Now().UnixNano()/(1000*1000), 10) 123 124 if s.topicName == "" { 125 s.topicName = "cloudprober-" + sysvars.Vars()["hostname"] 126 } 127 128 if s.gcpProject == "" && metadata.OnGCE() { 129 project, err := metadata.ProjectID() 130 if err != nil { 131 return fmt.Errorf("pubsub_surfacer: unable to retrieve project id: %v", err) 132 } 133 s.gcpProject = project 134 } 135 136 client, err := newPubsubClient(ctx, s.gcpProject) 137 if err != nil { 138 return fmt.Errorf("pubsub_surfacer: error creating pubsub client: %v", err) 139 } 140 141 s.topic = client.Topic(s.topicName) 142 exists, err := s.topic.Exists(ctx) 143 if err != nil { 144 return fmt.Errorf("pubsub_surfacer: error determining if topic (%s) exists: %v", s.topicName, err) 145 } 146 147 if !exists { 148 topic, err := client.CreateTopic(ctx, s.topicName) 149 if err != nil { 150 return fmt.Errorf("pubsub_surfacer: error creating topic (%s) for publishing: %v", s.topicName, err) 151 } 152 s.topic = topic 153 } 154 155 go func() { 156 for { 157 select { 158 case <-ctx.Done(): 159 s.topic.Stop() 160 return 161 case res, ok := <-s.publishResultChan: 162 if !ok { 163 return 164 } 165 _, err := res.Get(ctx) 166 if err != nil { 167 s.l.Warningf("Error publishing message: %v", err) 168 } 169 } 170 } 171 }() 172 173 if s.c.GetCompressionEnabled() { 174 s.compressionBuffer = compress.NewCompressionBuffer(ctx, func(data []byte) { 175 s.publishMessage(ctx, data) 176 }, s.opts.MetricsBufferSize/10, s.l) 177 } 178 179 // Start a goroutine to run forever, polling on the inChan. Allows 180 // for the surfacer to write asynchronously to the serial port. 181 s.processInputWg.Add(1) 182 go s.processInput(ctx) 183 184 return nil 185 } 186 187 // close closes the input channel, waits for input processing to finish, 188 // and closes the compression buffer if open. 189 func (s *Surfacer) close() { 190 close(s.inChan) 191 s.processInputWg.Wait() 192 193 if s.compressionBuffer != nil { 194 s.compressionBuffer.Close() 195 } 196 close(s.publishResultChan) 197 s.topic.Stop() 198 } 199 200 // Write queues the incoming data into a channel. This channel is watched by a 201 // goroutine that actually publishes it to a pubsub topic. 202 func (s *Surfacer) Write(ctx context.Context, em *metrics.EventMetrics) { 203 select { 204 case s.inChan <- em: 205 default: 206 s.l.Errorf("Surfacer's write channel (capacity: %d) is full, dropping new data.", s.opts.MetricsBufferSize) 207 } 208 } 209 210 // New initializes a Surfacer for publishing data to a pubsub topic. 211 func New(ctx context.Context, config *configpb.SurfacerConf, opts *options.Options, l *logger.Logger) (*Surfacer, error) { 212 s := &Surfacer{ 213 c: config, 214 opts: opts, 215 l: l, 216 topicName: config.GetTopicName(), 217 gcpProject: config.GetProject(), 218 publishResultChan: make(chan *pubsub.PublishResult, 1000), 219 } 220 221 return s, s.init(ctx) 222 }