mirror of
https://github.com/safing/portbase
synced 2025-09-01 10:09:50 +00:00
Merge pull request #215 from safing/feature/internal-metric-id
Improve Metrics and API
This commit is contained in:
commit
d777cd6809
10 changed files with 173 additions and 13 deletions
|
@ -436,6 +436,9 @@ func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Add response headers to request struct so that the endpoint can work with them.
|
||||
apiRequest.ResponseHeader = w.Header()
|
||||
|
||||
// Execute action function and get response data
|
||||
var responseData []byte
|
||||
var err error
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
)
|
||||
|
||||
|
@ -46,6 +47,7 @@ func registerDebugEndpoints() error {
|
|||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "debug/cpu",
|
||||
MimeType: "application/octet-stream",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: handleCPUProfile,
|
||||
Name: "Get CPU Profile",
|
||||
|
@ -67,6 +69,7 @@ You can easily view this data in your browser with this command (with Go install
|
|||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "debug/heap",
|
||||
MimeType: "application/octet-stream",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: handleHeapProfile,
|
||||
Name: "Get Heap Profile",
|
||||
|
@ -81,6 +84,7 @@ You can easily view this data in your browser with this command (with Go install
|
|||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "debug/allocs",
|
||||
MimeType: "application/octet-stream",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: handleAllocsProfile,
|
||||
Name: "Get Allocs Profile",
|
||||
|
@ -154,6 +158,12 @@ func handleCPUProfile(ar *Request) (data []byte, err error) {
|
|||
duration = parsedDuration
|
||||
}
|
||||
|
||||
// Indicate download and filename.
|
||||
ar.ResponseHeader.Set(
|
||||
"Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="portmaster-cpu-profile_v%s.pprof"`, info.Version()),
|
||||
)
|
||||
|
||||
// Start CPU profiling.
|
||||
buf := new(bytes.Buffer)
|
||||
if err := pprof.StartCPUProfile(buf); err != nil {
|
||||
|
@ -175,6 +185,12 @@ func handleCPUProfile(ar *Request) (data []byte, err error) {
|
|||
|
||||
// handleHeapProfile returns the Heap profile.
|
||||
func handleHeapProfile(ar *Request) (data []byte, err error) {
|
||||
// Indicate download and filename.
|
||||
ar.ResponseHeader.Set(
|
||||
"Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="portmaster-memory-heap-profile_v%s.pprof"`, info.Version()),
|
||||
)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := pprof.Lookup("heap").WriteTo(buf, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to write heap profile: %w", err)
|
||||
|
@ -184,6 +200,12 @@ func handleHeapProfile(ar *Request) (data []byte, err error) {
|
|||
|
||||
// handleAllocsProfile returns the Allocs profile.
|
||||
func handleAllocsProfile(ar *Request) (data []byte, err error) {
|
||||
// Indicate download and filename.
|
||||
ar.ResponseHeader.Set(
|
||||
"Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="portmaster-memory-allocs-profile_v%s.pprof"`, info.Version()),
|
||||
)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := pprof.Lookup("allocs").WriteTo(buf, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to write allocs profile: %w", err)
|
||||
|
|
|
@ -26,6 +26,9 @@ type Request struct {
|
|||
// AuthToken is the request-side authentication token assigned.
|
||||
AuthToken *AuthToken
|
||||
|
||||
// ResponseHeader holds the response header.
|
||||
ResponseHeader http.Header
|
||||
|
||||
// HandlerCache can be used by handlers to cache data between handlers within a request.
|
||||
HandlerCache interface{}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package metrics
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -17,20 +16,41 @@ import (
|
|||
func registerAPI() error {
|
||||
api.RegisterHandler("/metrics", &metricsAPI{})
|
||||
|
||||
return api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "metrics/list",
|
||||
Read: api.PermitAnyone,
|
||||
MimeType: api.MimeTypeJSON,
|
||||
BelongsTo: module,
|
||||
DataFunc: func(*api.Request) ([]byte, error) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
return json.Marshal(registry)
|
||||
},
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Export Registered Metrics",
|
||||
Description: "List all registered metrics with their metadata.",
|
||||
})
|
||||
Path: "metrics/list",
|
||||
Read: api.Dynamic,
|
||||
BelongsTo: module,
|
||||
StructFunc: func(ar *api.Request) (any, error) {
|
||||
return ExportMetrics(ar.AuthToken.Read), nil
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Export Metric Values",
|
||||
Description: "List all exportable metric values.",
|
||||
Path: "metrics/values",
|
||||
Read: api.Dynamic,
|
||||
Parameters: []api.Parameter{{
|
||||
Method: http.MethodGet,
|
||||
Field: "internal-only",
|
||||
Description: "Specify to only return metrics with an alternative internal ID.",
|
||||
}},
|
||||
BelongsTo: module,
|
||||
StructFunc: func(ar *api.Request) (any, error) {
|
||||
return ExportValues(
|
||||
ar.AuthToken.Read,
|
||||
ar.Request.URL.Query().Has("internal-only"),
|
||||
), nil
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type metricsAPI struct{}
|
||||
|
|
|
@ -43,6 +43,10 @@ type Options struct {
|
|||
// Name defines an optional human readable name for the metric.
|
||||
Name string
|
||||
|
||||
// InternalID specifies an alternative internal ID that will be used when
|
||||
// exposing the metric via the API in a structured format.
|
||||
InternalID string
|
||||
|
||||
// AlertLimit defines an upper limit that triggers an alert.
|
||||
AlertLimit float64
|
||||
|
||||
|
|
|
@ -42,3 +42,8 @@ func NewCounter(id string, labels map[string]string, opts *Options) (*Counter, e
|
|||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// CurrentValue returns the current counter value.
|
||||
func (c *Counter) CurrentValue() uint64 {
|
||||
return c.Get()
|
||||
}
|
||||
|
|
|
@ -50,6 +50,11 @@ func NewFetchingCounter(id string, labels map[string]string, fn func() uint64, o
|
|||
return m, nil
|
||||
}
|
||||
|
||||
// CurrentValue returns the current counter value.
|
||||
func (fc *FetchingCounter) CurrentValue() uint64 {
|
||||
return fc.fetchCnt()
|
||||
}
|
||||
|
||||
// WritePrometheus writes the metric in the prometheus format to the given writer.
|
||||
func (fc *FetchingCounter) WritePrometheus(w io.Writer) {
|
||||
fc.counter.Set(fc.fetchCnt())
|
||||
|
|
89
metrics/metric_export.go
Normal file
89
metrics/metric_export.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/api"
|
||||
)
|
||||
|
||||
// UIntMetric is an interface for special functions of uint metrics.
|
||||
type UIntMetric interface {
|
||||
CurrentValue() uint64
|
||||
}
|
||||
|
||||
// FloatMetric is an interface for special functions of float metrics.
|
||||
type FloatMetric interface {
|
||||
CurrentValue() float64
|
||||
}
|
||||
|
||||
// MetricExport is used to export a metric and its current value.
|
||||
type MetricExport struct {
|
||||
Metric
|
||||
CurrentValue any
|
||||
}
|
||||
|
||||
// ExportMetrics exports all registered metrics.
|
||||
func ExportMetrics(requestPermission api.Permission) []*MetricExport {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
export := make([]*MetricExport, 0, len(registry))
|
||||
for _, metric := range registry {
|
||||
// Check permission.
|
||||
if requestPermission < metric.Opts().Permission {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add metric with current value.
|
||||
export = append(export, &MetricExport{
|
||||
Metric: metric,
|
||||
CurrentValue: getCurrentValue(metric),
|
||||
})
|
||||
}
|
||||
|
||||
return export
|
||||
}
|
||||
|
||||
// ExportValues exports the values of all supported metrics.
|
||||
func ExportValues(requestPermission api.Permission, internalOnly bool) map[string]any {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
export := make(map[string]any, len(registry))
|
||||
for _, metric := range registry {
|
||||
// Check permission.
|
||||
if requestPermission < metric.Opts().Permission {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get Value.
|
||||
v := getCurrentValue(metric)
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get ID.
|
||||
var id string
|
||||
switch {
|
||||
case metric.Opts().InternalID != "":
|
||||
id = metric.Opts().InternalID
|
||||
case internalOnly:
|
||||
continue
|
||||
default:
|
||||
id = metric.LabeledID()
|
||||
}
|
||||
|
||||
// Add to export
|
||||
export[id] = v
|
||||
}
|
||||
|
||||
return export
|
||||
}
|
||||
|
||||
func getCurrentValue(metric Metric) any {
|
||||
if m, ok := metric.(UIntMetric); ok {
|
||||
return m.CurrentValue()
|
||||
}
|
||||
if m, ok := metric.(FloatMetric); ok {
|
||||
return m.CurrentValue()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -39,3 +39,8 @@ func NewGauge(id string, labels map[string]string, fn func() float64, opts *Opti
|
|||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// CurrentValue returns the current gauge value.
|
||||
func (g *Gauge) CurrentValue() float64 {
|
||||
return g.Get()
|
||||
}
|
||||
|
|
|
@ -92,6 +92,10 @@ func register(m Metric) error {
|
|||
if m.LabeledID() == registeredMetric.LabeledID() {
|
||||
return ErrAlreadyRegistered
|
||||
}
|
||||
if m.Opts().InternalID != "" &&
|
||||
m.Opts().InternalID == registeredMetric.Opts().InternalID {
|
||||
return fmt.Errorf("%w with this internal ID", ErrAlreadyRegistered)
|
||||
}
|
||||
}
|
||||
|
||||
// Add new metric to registry and sort it.
|
||||
|
|
Loading…
Add table
Reference in a new issue