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

feat: metrics for services and checks #519

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f76470c
poc: a metrics module for pebble
IronCore864 Nov 13, 2024
6d8ee59
chore: undo unnecessary change
IronCore864 Nov 14, 2024
b4abc9a
chore: undo unnecessary change
IronCore864 Nov 14, 2024
a274276
chore: undo unnecessary change
IronCore864 Nov 14, 2024
a2c07e6
chore: metrics identity basic auth poc
IronCore864 Nov 26, 2024
4ebb633
chore: a poc for metrics with labels
IronCore864 Nov 27, 2024
7468b95
poc: remove adding identities using env vars according to comment in …
IronCore864 Nov 28, 2024
790a8f9
chore: update tests for the metrics lib poc
IronCore864 Nov 28, 2024
272005b
chore: refactor identities and access according to spec review
IronCore864 Dec 9, 2024
5be3e96
feat: use sha512 to verify password
IronCore864 Jan 21, 2025
a6c374d
feat: move the metrics api to /v1/metrics
IronCore864 Jan 21, 2025
1bd54cb
chore: remove Username from apiBasicIdentity
IronCore864 Jan 21, 2025
98ea11e
chore: revert changes on user state
IronCore864 Jan 21, 2025
68c18b7
Merge branch 'master' into poc-custom-metrics-lib
IronCore864 Jan 21, 2025
7fc255e
chore: fix failed identity tests
IronCore864 Jan 21, 2025
31a0617
test: unit tests for basic identity
IronCore864 Jan 22, 2025
a57f041
chore: rework the metrics for services
IronCore864 Jan 24, 2025
b7a442f
chore: add metrics for checks, not done
IronCore864 Jan 24, 2025
8ebebb8
chore: refactor metrics, add open telemetry writer
IronCore864 Feb 11, 2025
363eaf0
Merge branch 'master' into poc-custom-metrics-lib
IronCore864 Feb 11, 2025
a1db1a6
chore: refactor according to review, fix check counter reset issue
IronCore864 Feb 11, 2025
047cc42
Merge branch 'master' into poc-custom-metrics-lib
IronCore864 Feb 11, 2025
c527344
chore: add a test for check metrics
IronCore864 Feb 12, 2025
5476299
test: add tests for open telemetry writer
IronCore864 Feb 12, 2025
9e5b65a
test: service metrics
IronCore864 Feb 12, 2025
d678766
chore: update tests
IronCore864 Feb 12, 2025
c99fa53
chore: fix linting
IronCore864 Feb 12, 2025
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this package maintained? Are we confident about introducing it in every charm / pebble deployment out there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It hasn't been recently changed, but the part of it we're using is small a straight-forward, and the algorithm being implemented is stable and well-documented (SHA-crypt). So I would say it's "stable" rather than "unmaintained". I'm actually contemplating vendoring this (the core part of it is only ~100 LoC). But open to other ideas/concerns too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've done some diligence on it, see #563 (comment) for example.

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 {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
w.WriteHeader(http.StatusInternalServerError)
return
}
err = r.chkMgr.Metrics(&buffer)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
w.Write([]byte(buffer.String()))
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
}
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()
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
// 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{}
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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, ",") + "}"
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
}

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