github.com/verrazzano/verrazzano@v1.7.1/tools/psr/backend/workers/opensearch/postlogs/postlogs.go (about)

     1  // Copyright (c) 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package postlogs
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strconv"
    13  	"sync/atomic"
    14  	"time"
    15  
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    18  	"github.com/verrazzano/verrazzano/pkg/security/password"
    19  	"github.com/verrazzano/verrazzano/tools/psr/backend/config"
    20  	"github.com/verrazzano/verrazzano/tools/psr/backend/metrics"
    21  	"github.com/verrazzano/verrazzano/tools/psr/backend/osenv"
    22  	"github.com/verrazzano/verrazzano/tools/psr/backend/spi"
    23  )
    24  
    25  const (
    26  	// metricsPrefix is the prefix that is automatically pre-pended to all metrics exported by this worker.
    27  	metricsPrefix = "opensearch_postlogs"
    28  
    29  	LogEntries      = "LOG_ENTRIES"
    30  	LogLength       = "LOG_LENGTH"
    31  	osIngestService = "vmi-system-os-ingest.verrazzano-system:9200"
    32  )
    33  
    34  // Use an http client interface so that we can override http.Client for unit tests
    35  type httpClientI interface {
    36  	Do(_ *http.Request) (resp *http.Response, err error)
    37  }
    38  
    39  var httpClient httpClientI = &http.Client{}
    40  var _ httpClientI = &http.Client{}
    41  
    42  type worker struct {
    43  	metricDescList []prometheus.Desc
    44  	*workerMetrics
    45  }
    46  
    47  var _ spi.Worker = worker{}
    48  
    49  // workerMetrics holds the metrics produced by the worker. Metrics must be thread safe.
    50  type workerMetrics struct {
    51  	openSearchPostSuccessCountTotal         metrics.MetricItem
    52  	openSearchPostFailureCountTotal         metrics.MetricItem
    53  	openSearchPostSuccessLatencyNanoSeconds metrics.MetricItem
    54  	openSearchPostFailureLatencyNanoSeconds metrics.MetricItem
    55  	openSearchPostDataCharsTotal            metrics.MetricItem
    56  }
    57  
    58  func NewPostLogsWorker() (spi.Worker, error) {
    59  	w := worker{workerMetrics: &workerMetrics{
    60  		openSearchPostSuccessCountTotal: metrics.MetricItem{
    61  			Name: "success_count_total",
    62  			Help: "The total number of successful OpenSearch POST requests",
    63  			Type: prometheus.CounterValue,
    64  		},
    65  		openSearchPostFailureCountTotal: metrics.MetricItem{
    66  			Name: "failure_count_total",
    67  			Help: "The total number of successful OpenSearch POST requests",
    68  			Type: prometheus.CounterValue,
    69  		},
    70  		openSearchPostSuccessLatencyNanoSeconds: metrics.MetricItem{
    71  			Name: "success_latency_nanoseconds",
    72  			Help: "The latency of successful OpenSearch POST requests in nanoseconds",
    73  			Type: prometheus.GaugeValue,
    74  		},
    75  		openSearchPostFailureLatencyNanoSeconds: metrics.MetricItem{
    76  			Name: "failure_latency_nanoseconds",
    77  			Help: "The latency of failed OpenSearch POST requests in nanoseconds",
    78  			Type: prometheus.GaugeValue,
    79  		},
    80  		openSearchPostDataCharsTotal: metrics.MetricItem{
    81  			Name: "data_chars_total",
    82  			Help: "The total number of characters posted to OpenSearch",
    83  			Type: prometheus.CounterValue,
    84  		},
    85  	}}
    86  
    87  	if err := config.PsrEnv.LoadFromEnv(w.GetEnvDescList()); err != nil {
    88  		return w, err
    89  	}
    90  
    91  	metricsLabels := map[string]string{
    92  		config.PsrWorkerTypeMetricsName: config.PsrEnv.GetEnv(config.PsrWorkerType),
    93  	}
    94  
    95  	w.metricDescList = metrics.BuildMetricDescList([]*metrics.MetricItem{
    96  		&w.openSearchPostSuccessCountTotal,
    97  		&w.openSearchPostFailureCountTotal,
    98  		&w.openSearchPostSuccessLatencyNanoSeconds,
    99  		&w.openSearchPostFailureLatencyNanoSeconds,
   100  		&w.openSearchPostDataCharsTotal,
   101  	}, metricsLabels, w.GetWorkerDesc().MetricsPrefix)
   102  
   103  	return w, nil
   104  }
   105  
   106  // GetWorkerDesc returns the WorkerDesc for the worker
   107  func (w worker) GetWorkerDesc() spi.WorkerDesc {
   108  	return spi.WorkerDesc{
   109  		WorkerType:    config.WorkerTypeOpsPostLogs,
   110  		Description:   "The postlogs worker performs POST requests on the OpenSearch endpoint",
   111  		MetricsPrefix: metricsPrefix,
   112  	}
   113  }
   114  
   115  func (w worker) GetEnvDescList() []osenv.EnvVarDesc {
   116  	return []osenv.EnvVarDesc{
   117  		{Key: LogEntries, DefaultVal: "1", Required: false},
   118  		{Key: LogLength, DefaultVal: "1", Required: false},
   119  	}
   120  }
   121  
   122  func (w worker) WantLoopInfoLogged() bool {
   123  	return false
   124  }
   125  
   126  func (w worker) PreconditionsMet() (bool, error) {
   127  	return true, nil
   128  }
   129  
   130  func (w worker) DoWork(conf config.CommonConfig, log vzlog.VerrazzanoLogger) error {
   131  	c := httpClient
   132  	logEntries, err := strconv.Atoi(config.PsrEnv.GetEnv(LogEntries))
   133  	if err != nil {
   134  		return err
   135  	}
   136  	logLength, err := strconv.Atoi(config.PsrEnv.GetEnv(LogLength))
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	body, bodyChars, err := getBody(logEntries, logLength)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	req := http.Request{
   147  		Method: "POST",
   148  		URL: &url.URL{
   149  			Scheme: "http",
   150  			Host:   osIngestService,
   151  			Path:   fmt.Sprintf("/verrazzano-application-%s/_bulk", conf.Namespace),
   152  		},
   153  		Header: http.Header{"Content-Type": {"application/json"}},
   154  		Body:   body,
   155  	}
   156  
   157  	startRequest := time.Now().UnixNano()
   158  	resp, err := c.Do(&req)
   159  	if err != nil {
   160  		atomic.AddInt64(&w.workerMetrics.openSearchPostFailureCountTotal.Val, 1)
   161  		return err
   162  	}
   163  	if resp == nil {
   164  		atomic.AddInt64(&w.workerMetrics.openSearchPostFailureCountTotal.Val, 1)
   165  		return fmt.Errorf("POST request to URI %s received a nil response", req.URL.RequestURI())
   166  	}
   167  
   168  	if resp.StatusCode != 200 && resp.StatusCode != 201 {
   169  		atomic.StoreInt64(&w.workerMetrics.openSearchPostFailureLatencyNanoSeconds.Val, time.Now().UnixNano()-startRequest)
   170  		atomic.AddInt64(&w.workerMetrics.openSearchPostFailureCountTotal.Val, 1)
   171  		return fmt.Errorf("OpenSearch POST request failed, returned %v status code with status: %s", resp.StatusCode, resp.Status)
   172  	}
   173  	atomic.StoreInt64(&w.workerMetrics.openSearchPostSuccessLatencyNanoSeconds.Val, time.Now().UnixNano()-startRequest)
   174  	atomic.AddInt64(&w.workerMetrics.openSearchPostSuccessCountTotal.Val, 1)
   175  	atomic.AddInt64(&w.workerMetrics.openSearchPostDataCharsTotal.Val, bodyChars)
   176  	return nil
   177  }
   178  
   179  func (w worker) GetMetricDescList() []prometheus.Desc {
   180  	return w.metricDescList
   181  }
   182  
   183  func (w worker) GetMetricList() []prometheus.Metric {
   184  	return []prometheus.Metric{
   185  		w.openSearchPostSuccessCountTotal.BuildMetric(),
   186  		w.openSearchPostFailureCountTotal.BuildMetric(),
   187  		w.openSearchPostSuccessLatencyNanoSeconds.BuildMetric(),
   188  		w.openSearchPostFailureLatencyNanoSeconds.BuildMetric(),
   189  		w.openSearchPostDataCharsTotal.BuildMetric(),
   190  	}
   191  }
   192  
   193  func getBody(logCount int, dataLength int) (io.ReadCloser, int64, error) {
   194  	var body string
   195  	for i := 0; i < logCount; i++ {
   196  		data, err := password.GeneratePassword(dataLength)
   197  		if err != nil {
   198  			return nil, 0, err
   199  		}
   200  		body = body + "{\"create\": {}}\n" + fmt.Sprintf("{\"postlogs-data\":\"%s\",\"@timestamp\":\"%v\"}\n", data, getTimestamp())
   201  	}
   202  	return io.NopCloser(bytes.NewBuffer([]byte(body))), int64(len(body)), nil
   203  }
   204  
   205  func getTimestamp() interface{} {
   206  	return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
   207  		time.Now().Year(),
   208  		int(time.Now().Month()),
   209  		time.Now().Day(),
   210  		time.Now().Hour(),
   211  		time.Now().Minute(),
   212  		time.Now().Second(),
   213  	)
   214  }