github.com/safing/portbase@v0.19.5/metrics/api.go (about)

     1  package metrics
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"time"
    10  
    11  	"github.com/safing/portbase/api"
    12  	"github.com/safing/portbase/config"
    13  	"github.com/safing/portbase/log"
    14  )
    15  
    16  func registerAPI() error {
    17  	api.RegisterHandler("/metrics", &metricsAPI{})
    18  
    19  	if err := api.RegisterEndpoint(api.Endpoint{
    20  		Name:        "Export Registered Metrics",
    21  		Description: "List all registered metrics with their metadata.",
    22  		Path:        "metrics/list",
    23  		Read:        api.Dynamic,
    24  		BelongsTo:   module,
    25  		StructFunc: func(ar *api.Request) (any, error) {
    26  			return ExportMetrics(ar.AuthToken.Read), nil
    27  		},
    28  	}); err != nil {
    29  		return err
    30  	}
    31  
    32  	if err := api.RegisterEndpoint(api.Endpoint{
    33  		Name:        "Export Metric Values",
    34  		Description: "List all exportable metric values.",
    35  		Path:        "metrics/values",
    36  		Read:        api.Dynamic,
    37  		Parameters: []api.Parameter{{
    38  			Method:      http.MethodGet,
    39  			Field:       "internal-only",
    40  			Description: "Specify to only return metrics with an alternative internal ID.",
    41  		}},
    42  		BelongsTo: module,
    43  		StructFunc: func(ar *api.Request) (any, error) {
    44  			return ExportValues(
    45  				ar.AuthToken.Read,
    46  				ar.Request.URL.Query().Has("internal-only"),
    47  			), nil
    48  		},
    49  	}); err != nil {
    50  		return err
    51  	}
    52  
    53  	return nil
    54  }
    55  
    56  type metricsAPI struct{}
    57  
    58  func (m *metricsAPI) ReadPermission(*http.Request) api.Permission { return api.Dynamic }
    59  
    60  func (m *metricsAPI) WritePermission(*http.Request) api.Permission { return api.NotSupported }
    61  
    62  func (m *metricsAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    63  	// Get API Request for permission and query.
    64  	ar := api.GetAPIRequest(r)
    65  	if ar == nil {
    66  		http.Error(w, "Missing API Request.", http.StatusInternalServerError)
    67  		return
    68  	}
    69  
    70  	// Get expertise level from query.
    71  	expertiseLevel := config.ExpertiseLevelDeveloper
    72  	switch ar.Request.URL.Query().Get("level") {
    73  	case config.ExpertiseLevelNameUser:
    74  		expertiseLevel = config.ExpertiseLevelUser
    75  	case config.ExpertiseLevelNameExpert:
    76  		expertiseLevel = config.ExpertiseLevelExpert
    77  	case config.ExpertiseLevelNameDeveloper:
    78  		expertiseLevel = config.ExpertiseLevelDeveloper
    79  	}
    80  
    81  	w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
    82  	w.WriteHeader(http.StatusOK)
    83  	WriteMetrics(w, ar.AuthToken.Read, expertiseLevel)
    84  }
    85  
    86  // WriteMetrics writes all metrics that match the given permission and
    87  // expertiseLevel to the given writer.
    88  func WriteMetrics(w io.Writer, permission api.Permission, expertiseLevel config.ExpertiseLevel) {
    89  	registryLock.RLock()
    90  	defer registryLock.RUnlock()
    91  
    92  	// Write all matching metrics.
    93  	for _, metric := range registry {
    94  		if permission >= metric.Opts().Permission &&
    95  			expertiseLevel >= metric.Opts().ExpertiseLevel {
    96  			metric.WritePrometheus(w)
    97  		}
    98  	}
    99  }
   100  
   101  func writeMetricsTo(ctx context.Context, url string) error {
   102  	// First, collect metrics into buffer.
   103  	buf := &bytes.Buffer{}
   104  	WriteMetrics(buf, api.PermitSelf, config.ExpertiseLevelDeveloper)
   105  
   106  	// Check if there is something to send.
   107  	if buf.Len() == 0 {
   108  		log.Debugf("metrics: not pushing metrics, nothing to send")
   109  		return nil
   110  	}
   111  
   112  	// Create request
   113  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, buf)
   114  	if err != nil {
   115  		return fmt.Errorf("failed to create request: %w", err)
   116  	}
   117  
   118  	// Send.
   119  	resp, err := http.DefaultClient.Do(req)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	defer func() {
   124  		_ = resp.Body.Close()
   125  	}()
   126  
   127  	// Check return status.
   128  	if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
   129  		return nil
   130  	}
   131  
   132  	// Get and return error.
   133  	body, _ := io.ReadAll(resp.Body)
   134  	return fmt.Errorf(
   135  		"got %s while writing metrics to %s: %s",
   136  		resp.Status,
   137  		url,
   138  		body,
   139  	)
   140  }
   141  
   142  func metricsWriter(ctx context.Context) error {
   143  	pushURL := pushOption()
   144  	ticker := module.NewSleepyTicker(1*time.Minute, 0)
   145  	defer ticker.Stop()
   146  
   147  	for {
   148  		select {
   149  		case <-ctx.Done():
   150  			return nil
   151  		case <-ticker.Wait():
   152  			err := writeMetricsTo(ctx, pushURL)
   153  			if err != nil {
   154  				return err
   155  			}
   156  		}
   157  	}
   158  }