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 }