Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc: a metrics module for pebble #519

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion client/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type Identity struct {
Access IdentityAccess `json:"access" yaml:"access"`

// One or more of the following type-specific configuration fields must be
// non-nil (currently the only type is "local").
// non-nil (currently the only types are "local" and "basic").
Local *LocalIdentity `json:"local,omitempty" yaml:"local,omitempty"`
Basic *BasicIdentity `json:"basic,omitempty" yaml:"basic,omitempty"`
}

// IdentityAccess defines the access level for an identity.
Expand All @@ -47,6 +48,12 @@ type LocalIdentity struct {
UserID *uint32 `json:"user-id" yaml:"user-id"`
}

// BasicIdentity holds identity configuration specific to the "basic" type
// (for username/password authentication).
type BasicIdentity struct {
Password string `json:"password" yaml:"password"`
}

// For future extension.
type IdentitiesOptions struct{}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/canonical/pebble
go 1.22

require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b
github.com/gorilla/mux v1.8.1
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E=
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU=
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVEYtrzBLTtyzoyU3nIS0Cf0GvjmwU=
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
Expand All @@ -15,6 +19,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk=
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
3 changes: 3 additions & 0 deletions internals/cli/cmd_identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ func (cmd *cmdIdentities) writeText(identities map[string]*client.Identity) erro
if identity.Local != nil {
types = append(types, "local")
}
if identity.Basic != nil {
types = append(types, "basic")
}
sort.Strings(types)
if len(types) == 0 {
types = append(types, "unknown")
Expand Down
15 changes: 15 additions & 0 deletions internals/daemon/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,18 @@ func (ac UserAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Re
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}

// MetricsAccess allows requests over the HTTP from authenticated users
type MetricsAccess struct{}

func (ac MetricsAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Response {
if user == nil {
return Unauthorized(accessDenied)
}
switch user.Access {
case state.MetricsAccess, state.AdminAccess:
return nil
}
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}
5 changes: 5 additions & 0 deletions internals/daemon/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,18 @@ var API = []*Command{{
WriteAccess: AdminAccess{},
GET: v1GetIdentities,
POST: v1PostIdentities,
}, {
Path: "/v1/metrics",
ReadAccess: MetricsAccess{},
GET: v1GetMetrics,
}}

var (
stateEnsureBefore = (*state.State).EnsureBefore

overlordServiceManager = (*overlord.Overlord).ServiceManager
overlordPlanManager = (*overlord.Overlord).PlanManager
overlordCheckManager = (*overlord.Overlord).CheckManager

muxVars = mux.Vars
)
Expand Down
53 changes: 53 additions & 0 deletions internals/daemon/api_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2024 Canonical Ltd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package daemon

import (
"bytes"
"net/http"

"github.com/canonical/pebble/internals/overlord/checkstate"
"github.com/canonical/pebble/internals/overlord/servstate"
)

func v1GetMetrics(c *Command, r *http.Request, _ *UserState) Response {
return metricsResponse{
svcMgr: overlordServiceManager(c.d.overlord),
chkMgr: overlordCheckManager(c.d.overlord),
}
}

// metricsResponse is a Response implementation to serve the metrics in the OpenMetrics format.
type metricsResponse struct {
svcMgr *servstate.ServiceManager
chkMgr *checkstate.CheckManager
}

func (r metricsResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var buffer bytes.Buffer

err := r.svcMgr.Metrics(&buffer)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = r.chkMgr.Metrics(&buffer)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(buffer.String()))
}
38 changes: 30 additions & 8 deletions internals/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,22 +147,27 @@ const (
accessForbidden
)

func userFromRequest(st *state.State, r *http.Request, ucred *Ucrednet) (*UserState, error) {
if ucred == nil {
// No ucred details, no UserState. Currently, "local" (ucred-based) is
// the only type of identity we support.
return nil, nil
func userFromRequest(st *state.State, r *http.Request, ucred *Ucrednet, username, password string) (*UserState, error) {
var userID *uint32
if ucred != nil {
userID = &ucred.Uid
}

st.Lock()
identity := st.IdentityFromInputs(&ucred.Uid)
identity := st.IdentityFromInputs(userID, username, password)
st.Unlock()

if identity == nil {
// No identity that matches these inputs (for now, just UID).
return nil, nil
}
return &UserState{Access: identity.Access, UID: &ucred.Uid}, nil

if identity.Local != nil {
return &UserState{Access: identity.Access, UID: userID}, nil
} else if identity.Basic != nil {
return &UserState{Access: identity.Access}, nil
}
return nil, nil
}

func (d *Daemon) Overlord() *overlord.Overlord {
Expand Down Expand Up @@ -213,7 +218,8 @@ func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// not good: https://github.com/canonical/pebble/pull/369
var user *UserState
if _, isOpen := access.(OpenAccess); !isOpen {
user, err = userFromRequest(c.d.state, r, ucred)
basicAuthUsername, basicAuthPassword, _ := r.BasicAuth()
user, err = userFromRequest(c.d.state, r, ucred, basicAuthUsername, basicAuthPassword)
if err != nil {
Forbidden("forbidden").ServeHTTP(w, r)
return
Expand Down Expand Up @@ -367,6 +373,22 @@ func (d *Daemon) Init() error {
}

logger.Noticef("Started daemon.")

// registry := metrics.GetRegistry()
// myCounter := registry.NewCounterVec("my_counter", "Total number of something processed.", []string{"operation", "status"})
// myGauge := registry.NewGaugeVec("my_gauge", "Current value of something.", []string{"sensor"})
// // Goroutine to update metrics randomly
// go func() {
// for {
// myCounter.WithLabelValues("read", "success").Inc()
// myCounter.WithLabelValues("write", "success").Add(2)
// myCounter.WithLabelValues("read", "failed").Inc()
// myGauge.WithLabelValues("temperature").Set(20.0 + rand.Float64()*10.0)

// time.Sleep(time.Duration(rand.Intn(5)+1) * time.Second) // Random sleep between 1 and 5 seconds
// }
// }()

return nil
}

Expand Down
50 changes: 50 additions & 0 deletions internals/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2025 Canonical Ltd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package metrics

import (
"fmt"
"io"
"sort"
"strings"
)

// Metric represents a single metric.
type Metric struct {
Name string
Value interface{}
LabelPairs []string
}

// WriteTo writes the metric in OpenMetrics format.
func (m *Metric) WriteTo(w io.Writer) (n int64, err error) {
labelStr := ""
if len(m.LabelPairs) > 0 {
sort.Strings(m.LabelPairs)
labelStr = "{" + strings.Join(m.LabelPairs, ",") + "}"
}

var written int
switch v := m.Value.(type) {
case int64:
written, err = fmt.Fprintf(w, "%s%s %d\n", m.Name, labelStr, v)
case float64:
written, err = fmt.Fprintf(w, "%s%s %.2f\n", m.Name, labelStr, v) // Format float appropriately
default:
written, err = fmt.Fprintf(w, "%s%s %v\n", m.Name, labelStr, m.Value)
}

return int64(written), err
}
2 changes: 2 additions & 0 deletions internals/overlord/checkstate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (m *CheckManager) doPerformCheck(task *state.Task, tomb *tombpkg.Tomb) erro
select {
case <-ticker.C:
err := runCheck(tomb.Context(nil), chk, config.Timeout.Value)
m.incCheckInfoPerformCheckCount(config)
if !tomb.Alive() {
return checkStopped(config.Name, task.Kind(), tomb.Err())
}
Expand Down Expand Up @@ -129,6 +130,7 @@ func (m *CheckManager) doRecoverCheck(task *state.Task, tomb *tombpkg.Tomb) erro
select {
case <-ticker.C:
err := runCheck(tomb.Context(nil), chk, config.Timeout.Value)
m.incCheckInfoRecoverCheckCount(config)
if !tomb.Alive() {
return checkStopped(config.Name, task.Kind(), tomb.Err())
}
Expand Down
Loading
Loading