mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-28 16:01:18 -05:00
groupware: jmap: add metrics
This commit is contained in:
@@ -22,19 +22,20 @@ type BlobClient interface {
|
||||
}
|
||||
|
||||
const (
|
||||
logOperation = "operation"
|
||||
logUsername = "username"
|
||||
logAccountId = "account-id"
|
||||
logMailboxId = "mailbox-id"
|
||||
logFetchBodies = "fetch-bodies"
|
||||
logOffset = "offset"
|
||||
logLimit = "limit"
|
||||
logApiUrl = "apiurl"
|
||||
logDownloadUrl = "downloadurl"
|
||||
logBlobId = "blobId"
|
||||
logUploadUrl = "downloadurl"
|
||||
logSessionState = "session-state"
|
||||
logSince = "since"
|
||||
logHttpStatusCode = "status"
|
||||
logOperation = "operation"
|
||||
logUsername = "username"
|
||||
logAccountId = "account-id"
|
||||
logMailboxId = "mailbox-id"
|
||||
logFetchBodies = "fetch-bodies"
|
||||
logOffset = "offset"
|
||||
logLimit = "limit"
|
||||
logApiUrl = "apiurl"
|
||||
logDownloadUrl = "downloadurl"
|
||||
logBlobId = "blobId"
|
||||
logUploadUrl = "downloadurl"
|
||||
logSessionState = "session-state"
|
||||
logSince = "since"
|
||||
|
||||
defaultAccountId = "*"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
wellKnown SessionClient
|
||||
session SessionClient
|
||||
api ApiClient
|
||||
blob BlobClient
|
||||
sessionEventListeners *eventListeners[SessionEventListener]
|
||||
@@ -19,9 +19,9 @@ func (j *Client) Close() error {
|
||||
return j.api.Close()
|
||||
}
|
||||
|
||||
func NewClient(wellKnown SessionClient, api ApiClient, blob BlobClient) Client {
|
||||
func NewClient(sessionClient SessionClient, api ApiClient, blob BlobClient) Client {
|
||||
return Client{
|
||||
wellKnown: wellKnown,
|
||||
session: sessionClient,
|
||||
api: api,
|
||||
blob: blob,
|
||||
sessionEventListeners: newEventListeners[SessionEventListener](),
|
||||
@@ -40,7 +40,7 @@ func (j *Client) onSessionOutdated(session *Session, newSessionState SessionStat
|
||||
|
||||
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
|
||||
func (j *Client) FetchSession(username string, logger *log.Logger) (Session, Error) {
|
||||
wk, err := j.wellKnown.GetSession(username, logger)
|
||||
wk, err := j.session.GetSession(username, logger)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
)
|
||||
@@ -20,6 +22,7 @@ type HttpJmapApiClient struct {
|
||||
masterUser string
|
||||
masterPassword string
|
||||
userAgent string
|
||||
metrics HttpJmapApiClientMetrics
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -34,13 +37,20 @@ func bearer(req *http.Request, token string) {
|
||||
}
|
||||
*/
|
||||
|
||||
func NewHttpJmapApiClient(baseurl url.URL, client *http.Client, masterUser string, masterPassword string) *HttpJmapApiClient {
|
||||
type HttpJmapApiClientMetrics struct {
|
||||
SuccessfulRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestStatusPerEndpointCounter *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func NewHttpJmapApiClient(baseurl url.URL, client *http.Client, masterUser string, masterPassword string, metrics HttpJmapApiClientMetrics) *HttpJmapApiClient {
|
||||
return &HttpJmapApiClient{
|
||||
baseurl: baseurl,
|
||||
client: client,
|
||||
masterUser: masterUser,
|
||||
masterPassword: masterPassword,
|
||||
userAgent: "OpenCloud/" + version.GetString(),
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +77,12 @@ func (h *HttpJmapApiClient) auth(username string, _ *log.Logger, req *http.Reque
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
wellKnownUrl := h.baseurl.JoinPath(".well-known", "jmap").String()
|
||||
sessionUrl := h.baseurl.JoinPath(".well-known", "jmap")
|
||||
sessionUrlStr := sessionUrl.String()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, wellKnownUrl, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, sessionUrlStr, nil)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create GET request for %v", wellKnownUrl)
|
||||
logger.Error().Err(err).Msgf("failed to create GET request for %v", sessionUrl)
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
|
||||
}
|
||||
h.auth(username, logger, req)
|
||||
@@ -79,13 +90,23 @@ func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (Ses
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", wellKnownUrl)
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(endpointOf(sessionUrl)).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", sessionUrl)
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 200")
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(endpointOf(sessionUrl), strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str(logHttpStatusCode, res.Status).Msg("HTTP response status code is not 200")
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP API response status is %v", res.Status)}
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(endpointOf(sessionUrl)).Inc()
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
@@ -104,7 +125,7 @@ func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (Ses
|
||||
var data SessionResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
logger.Error().Str("url", wellKnownUrl).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
|
||||
logger.Error().Str("url", sessionUrlStr).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
}
|
||||
|
||||
@@ -131,10 +152,16 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(session.JmapEndpoint).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
|
||||
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(session.JmapEndpoint, strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
@@ -146,6 +173,9 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
}
|
||||
}(res.Body)
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(session.JmapEndpoint).Inc()
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
@@ -168,10 +198,16 @@ func (h *HttpJmapApiClient) UploadBinary(ctx context.Context, logger *log.Logger
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(session.UploadEndpoint).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl)
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(session.UploadEndpoint, strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
@@ -183,6 +219,9 @@ func (h *HttpJmapApiClient) UploadBinary(ctx context.Context, logger *log.Logger
|
||||
}
|
||||
}(res.Body)
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(session.UploadEndpoint).Inc()
|
||||
}
|
||||
|
||||
responseBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
@@ -211,6 +250,9 @@ func (h *HttpJmapApiClient) DownloadBinary(ctx context.Context, logger *log.Logg
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(session.DownloadEndpoint).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl)
|
||||
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
@@ -218,9 +260,15 @@ func (h *HttpJmapApiClient) DownloadBinary(ctx context.Context, logger *log.Logg
|
||||
return nil, nil
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(session.DownloadEndpoint, strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(session.DownloadEndpoint).Inc()
|
||||
}
|
||||
|
||||
sizeStr := res.Header.Get("Content-Length")
|
||||
size := -1
|
||||
|
||||
@@ -231,7 +231,7 @@ type SessionAccount struct {
|
||||
IsPersonal bool `json:"isPersonal"`
|
||||
// This is true if the entire account is read-only.
|
||||
IsReadOnly bool `json:"isReadOnly"`
|
||||
AccountCapabilities SessionAccountCapabilities `json:"accountCapabilities,omitempty"`
|
||||
AccountCapabilities SessionAccountCapabilities `json:"accountCapabilities"`
|
||||
}
|
||||
|
||||
type SessionCoreCapabilities struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package jmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
@@ -36,12 +37,18 @@ type Session struct {
|
||||
|
||||
// The base URL to use for JMAP operations towards Stalwart
|
||||
JmapUrl url.URL
|
||||
// An identifier of the JmapUrl to use in metrics and tracing
|
||||
JmapEndpoint string
|
||||
|
||||
// The upload URL template
|
||||
UploadUrlTemplate string
|
||||
// An identifier of the UploadUrlTemplate to use in metrics and tracing
|
||||
UploadEndpoint string
|
||||
|
||||
// The upload URL template
|
||||
DownloadUrlTemplate string
|
||||
// An identifier of the DownloadUrlTemplate to use in metrics and tracing
|
||||
DownloadEndpoint string
|
||||
|
||||
SessionResponse
|
||||
}
|
||||
@@ -68,20 +75,28 @@ func newSession(sessionResponse SessionResponse) (Session, Error) {
|
||||
if err != nil {
|
||||
return Session{}, invalidSessionResponseErrorInvalidApiUrl
|
||||
}
|
||||
apiEndpoint := endpointOf(apiUrl)
|
||||
|
||||
uploadUrl := sessionResponse.UploadUrl
|
||||
if uploadUrl == "" {
|
||||
return Session{}, invalidSessionResponseErrorMissingUploadUrl
|
||||
}
|
||||
uploadEndpoint := toEndpoint(uploadUrl)
|
||||
|
||||
downloadUrl := sessionResponse.DownloadUrl
|
||||
if downloadUrl == "" {
|
||||
return Session{}, invalidSessionResponseErrorMissingDownloadUrl
|
||||
}
|
||||
downloadEndpoint := toEndpoint(downloadUrl)
|
||||
|
||||
return Session{
|
||||
Username: username,
|
||||
JmapUrl: *apiUrl,
|
||||
JmapEndpoint: apiEndpoint,
|
||||
UploadUrlTemplate: uploadUrl,
|
||||
UploadEndpoint: uploadEndpoint,
|
||||
DownloadUrlTemplate: downloadUrl,
|
||||
DownloadEndpoint: downloadEndpoint,
|
||||
SessionResponse: sessionResponse,
|
||||
}, nil
|
||||
}
|
||||
@@ -117,3 +132,20 @@ func (s Session) DecorateLogger(l log.Logger) *log.Logger {
|
||||
Str(logApiUrl, s.ApiUrl).
|
||||
Str(logSessionState, string(s.State)))
|
||||
}
|
||||
|
||||
func endpointOf(u *url.URL) string {
|
||||
if u != nil {
|
||||
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toEndpoint(str string) string {
|
||||
u, err := url.Parse(str)
|
||||
if err == nil {
|
||||
return endpointOf(u)
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user