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  }