mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-13 00:52:01 -04:00
groupware: add body tracing to WS, fix metrics registrations, add operation identifiers for metrics
This commit is contained in:
@@ -26,7 +26,7 @@ func (c Context) WithContext(newContext context.Context) Context {
|
||||
}
|
||||
|
||||
type ApiClient interface {
|
||||
Command(request Request, ctx Context) (io.ReadCloser, Language, Error)
|
||||
Command(operation Operation, request Request, ctx Context) (io.ReadCloser, Language, Error)
|
||||
io.Closer
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ type SessionClient interface {
|
||||
}
|
||||
|
||||
type BlobClient interface {
|
||||
UploadBinary(uploadUrl string, endpoint string, contentType string, content io.Reader, ctx Context) (UploadedBlob, Language, Error)
|
||||
DownloadBinary(downloadUrl string, endpoint string, ctx Context) (*BlobDownload, Language, Error)
|
||||
UploadBinary(uploadUrl string, operation Operation, endpoint string, contentType string, content io.Reader, ctx Context) (UploadedBlob, Language, Error)
|
||||
DownloadBinary(downloadUrl string, operation Operation, endpoint string, ctx Context) (*BlobDownload, Language, Error)
|
||||
io.Closer
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func (j *Client) GetBlobMetadata(accountId AccountId, ids []string, ctx Context)
|
||||
return ZeroResultV[BlobGetResponse](), jerr
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (BlobGetResponse, State, Error) {
|
||||
return command(j, Operation("GetBlobMetadata"), ctx, cmd, func(body *Response) (BlobGetResponse, State, Error) {
|
||||
var response BlobGetResponse
|
||||
err := retrieveGet(ctx, body, get, "0", &response)
|
||||
if err != nil {
|
||||
@@ -48,7 +48,7 @@ func (j *Client) UploadBlobStream(accountId AccountId, contentType string, body
|
||||
uploadUrl := strings.NewReplacer(
|
||||
"{accountId}", url.PathEscape(string(accountId)),
|
||||
).Replace(ctx.Session.UploadUrlTemplate)
|
||||
return j.blob.UploadBinary(uploadUrl, ctx.Session.UploadEndpoint, contentType, body, ctx)
|
||||
return j.blob.UploadBinary(uploadUrl, Operation("UploadBlobStream"), ctx.Session.UploadEndpoint, contentType, body, ctx)
|
||||
}
|
||||
|
||||
func (j *Client) DownloadBlobStream(accountId AccountId, blobId string, name string, typ string, ctx Context) (*BlobDownload, Language, error) { //NOSONAR
|
||||
@@ -61,7 +61,7 @@ func (j *Client) DownloadBlobStream(accountId AccountId, blobId string, name str
|
||||
"{type}", url.PathEscape(typ),
|
||||
).Replace(ctx.Session.DownloadUrlTemplate)
|
||||
logger = log.From(logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId))
|
||||
return j.blob.DownloadBinary(downloadUrl, ctx.Session.DownloadEndpoint, ctx)
|
||||
return j.blob.DownloadBinary(downloadUrl, Operation("DownloadBlobStream"), ctx.Session.DownloadEndpoint, ctx)
|
||||
}
|
||||
|
||||
func (j *Client) UploadBlob(accountId AccountId, data []byte, contentType string, ctx Context) (Result[UploadedBlobWithHash], error) {
|
||||
@@ -97,7 +97,7 @@ func (j *Client) UploadBlob(accountId AccountId, data []byte, contentType string
|
||||
return ZeroResultV[UploadedBlobWithHash](), jerr
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (UploadedBlobWithHash, State, Error) {
|
||||
return command(j, Operation("UploadBlob"), ctx, cmd, func(body *Response) (UploadedBlobWithHash, State, Error) {
|
||||
var uploadResponse BlobUploadResponse
|
||||
err := retrieveUpload(ctx, body, upload, "0", &uploadResponse)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ func (j *Client) GetBootstrap(accountIds []AccountId, ctx Context) (Result[map[A
|
||||
if err != nil {
|
||||
return ZeroResultV[map[AccountId]AccountBootstrapResult](), err
|
||||
}
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]AccountBootstrapResult, State, Error) {
|
||||
return command(j, Operation("GetBootstrap"), ctx, cmd, func(body *Response) (map[AccountId]AccountBootstrapResult, State, Error) {
|
||||
identityPerAccount := map[AccountId][]Identity{}
|
||||
quotaPerAccount := map[AccountId][]Quota{}
|
||||
identityStatesPerAccount := map[AccountId]State{}
|
||||
|
||||
@@ -13,7 +13,7 @@ func (j *Client) ParseICalendarBlob(accountId AccountId, blobIds []string, ctx C
|
||||
return ZeroResultV[CalendarEventParseResponse](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (CalendarEventParseResponse, State, Error) {
|
||||
return command(j, Operation("ParseICalendarBlob"), ctx, cmd, func(body *Response) (CalendarEventParseResponse, State, Error) {
|
||||
var response CalendarEventParseResponse
|
||||
err = retrieveParse(ctx, body, parse, "0", &response)
|
||||
if err != nil {
|
||||
|
||||
@@ -110,7 +110,7 @@ func (j *Client) GetChanges(accountId AccountId, stateMap StateMap, maxChanges u
|
||||
return ZeroResultV[ObjectChanges](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (ObjectChanges, State, Error) {
|
||||
return command(j, Operation("GetChanges"), ctx, cmd, func(body *Response) (ObjectChanges, State, Error) {
|
||||
changes := ObjectChanges{
|
||||
MaxChanges: maxChanges,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (j *Client) GetEmails(accountId AccountId, ids []string, //NOSONAR
|
||||
if err != nil {
|
||||
return ZeroResultV[EmailGetResponse](), err
|
||||
}
|
||||
return command(j, ctx, cmd, func(body *Response) (EmailGetResponse, State, Error) {
|
||||
return command(j, Operation("GetEmails"), ctx, cmd, func(body *Response) (EmailGetResponse, State, Error) {
|
||||
if markAsSeen {
|
||||
var markResponse EmailSetResponse
|
||||
err = retrieveSet(ctx, body, markEmails, "0", &markResponse)
|
||||
@@ -91,7 +91,7 @@ func (j *Client) GetEmailBlobId(accountId AccountId, id string, ctx Context) (Re
|
||||
if err != nil {
|
||||
return ZeroResultV[string](), err
|
||||
}
|
||||
return command(j, ctx, cmd, func(body *Response) (string, State, Error) {
|
||||
return command(j, Operation("GetEmailBlobId"), ctx, cmd, func(body *Response) (string, State, Error) {
|
||||
var response EmailGetResponse
|
||||
err = retrieveGet(ctx, body, get, "0", &response)
|
||||
if err != nil {
|
||||
@@ -175,7 +175,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId AccountId, mailboxId string, //
|
||||
return ZeroResultV[*EmailSearchResults](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (*EmailSearchResults, State, Error) {
|
||||
return command(j, Operation("GetAllEmailsInMailbox"), ctx, cmd, func(body *Response) (*EmailSearchResults, State, Error) {
|
||||
var queryResponse EmailQueryResponse
|
||||
err = retrieveQuery(ctx, body, query, "0", &queryResponse)
|
||||
if err != nil {
|
||||
@@ -262,7 +262,7 @@ func (j *Client) GetEmailChanges(accountId AccountId,
|
||||
return ZeroResultV[EmailChanges](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (EmailChanges, State, Error) {
|
||||
return command(j, Operation("GetEmailChanges"), ctx, cmd, func(body *Response) (EmailChanges, State, Error) {
|
||||
var changesResponse EmailChangesResponse
|
||||
err = retrieveChanges(ctx, body, changes, "0", &changesResponse)
|
||||
if err != nil {
|
||||
@@ -361,7 +361,7 @@ func (j *Client) QueryEmailSnippets(accountIds []AccountId, //NOSONAR
|
||||
return ZeroResultV[map[AccountId]EmailSnippetSearchResults](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailSnippetSearchResults, State, Error) {
|
||||
return command(j, Operation("QueryEmailSnippets"), ctx, cmd, func(body *Response) (map[AccountId]EmailSnippetSearchResults, State, Error) {
|
||||
results := make(map[AccountId]EmailSnippetSearchResults, len(uniqueAccountIds))
|
||||
states := make(map[AccountId]State, len(uniqueAccountIds))
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
@@ -473,7 +473,7 @@ func (j *Client) QueryEmails(accountIds []AccountId,
|
||||
return ZeroResultV[map[AccountId]EmailSearchResults](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailSearchResults, State, Error) {
|
||||
return command(j, Operation("QueryEmails"), ctx, cmd, func(body *Response) (map[AccountId]EmailSearchResults, State, Error) {
|
||||
results := make(map[AccountId]EmailSearchResults, len(uniqueAccountIds))
|
||||
queryStates := map[AccountId]State{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
@@ -568,7 +568,7 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []AccountId, //NOSONAR
|
||||
return ZeroResultV[map[AccountId]EmailQueryWithSnippetsResult](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailQueryWithSnippetsResult, State, Error) {
|
||||
return command(j, Operation("QueryEmailsWithSnippets"), ctx, cmd, func(body *Response) (map[AccountId]EmailQueryWithSnippetsResult, State, Error) {
|
||||
result := make(map[AccountId]EmailQueryWithSnippetsResult, len(uniqueAccountIds))
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
var queryResponse EmailQueryResponse
|
||||
@@ -662,7 +662,7 @@ func (j *Client) ImportEmail(accountId AccountId, data []byte, ctx Context) (Res
|
||||
return ZeroResultV[UploadedEmail](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (UploadedEmail, State, Error) {
|
||||
return command(j, Operation("ImportEmail"), ctx, cmd, func(body *Response) (UploadedEmail, State, Error) {
|
||||
var uploadResponse BlobUploadResponse
|
||||
err = retrieveResponseMatchParameters(ctx, body, CommandBlobUpload, "0", &uploadResponse)
|
||||
if err != nil {
|
||||
@@ -720,7 +720,7 @@ func (j *Client) CreateEmail(accountId AccountId, email EmailChange, replaceId s
|
||||
return ZeroResultV[*Email](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (*Email, State, Error) {
|
||||
return command(j, Operation("CreateEmail"), ctx, cmd, func(body *Response) (*Email, State, Error) {
|
||||
var setResponse EmailSetResponse
|
||||
err = retrieveResponseMatchParameters(ctx, body, CommandEmailSet, "0", &setResponse)
|
||||
if err != nil {
|
||||
@@ -767,7 +767,7 @@ func (j *Client) UpdateEmails(accountId AccountId, updates map[string]PatchObjec
|
||||
return ZeroResultV[map[string]*Email](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[string]*Email, State, Error) {
|
||||
return command(j, Operation("UpdateEmails"), ctx, cmd, func(body *Response) (map[string]*Email, State, Error) {
|
||||
var setResponse EmailSetResponse
|
||||
err = retrieveSet(ctx, body, set, "0", &setResponse)
|
||||
if err != nil {
|
||||
@@ -881,7 +881,7 @@ func (j *Client) SubmitEmail(accountId AccountId, identityId string, emailId str
|
||||
return ZeroResultV[EmailSubmission](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (EmailSubmission, State, Error) {
|
||||
return command(j, Operation("SubmitEmail"), ctx, cmd, func(body *Response) (EmailSubmission, State, Error) {
|
||||
var submissionResponse EmailSubmissionSetResponse
|
||||
err = retrieveSet(ctx, body, submit, "0", &submissionResponse)
|
||||
if err != nil {
|
||||
@@ -939,7 +939,7 @@ func (j *Client) GetEmailSubmissionStatus(accountId AccountId, submissionIds []s
|
||||
return ZeroResultV[EmailSubmissionGetResponse](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (EmailSubmissionGetResponse, State, Error) {
|
||||
return command(j, Operation("GetEmailSubmissionStatus"), ctx, cmd, func(body *Response) (EmailSubmissionGetResponse, State, Error) {
|
||||
var response EmailSubmissionGetResponse
|
||||
err = retrieveGet(ctx, body, get, "0", &response)
|
||||
if err != nil {
|
||||
@@ -981,7 +981,7 @@ func (j *Client) EmailsInThread(accountId AccountId, threadId string,
|
||||
return ZeroResultV[[]Email](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) ([]Email, State, Error) {
|
||||
return command(j, Operation("EmailsInThread"), ctx, cmd, func(body *Response) ([]Email, State, Error) {
|
||||
var emailsResponse EmailGetResponse
|
||||
err = retrieveGet(ctx, body, get, "1", &emailsResponse)
|
||||
if err != nil {
|
||||
@@ -1070,7 +1070,7 @@ func (j *Client) QueryEmailSummaries(accountIds []AccountId, //NOSONAR
|
||||
return ZeroResultV[map[AccountId]EmailsSummary](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailsSummary, State, Error) {
|
||||
return command(j, Operation("QueryEmailSummaries"), ctx, cmd, func(body *Response) (map[AccountId]EmailsSummary, State, Error) {
|
||||
resp := map[AccountId]EmailsSummary{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
var queryResponse EmailQueryResponse
|
||||
|
||||
@@ -55,7 +55,7 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId AccountId, accountId
|
||||
if err != nil {
|
||||
return ZeroResultV[IdentitiesAndMailboxesGetResponse](), err
|
||||
}
|
||||
return command(j, ctx, cmd, func(body *Response) (IdentitiesAndMailboxesGetResponse, State, Error) {
|
||||
return command(j, Operation("GetIdentitiesAndMailboxes"), ctx, cmd, func(body *Response) (IdentitiesAndMailboxesGetResponse, State, Error) {
|
||||
identities := make(map[AccountId][]Identity, len(uniqueAccountIds))
|
||||
stateByAccountId := make(map[AccountId]State, len(uniqueAccountIds))
|
||||
notFound := []string{}
|
||||
|
||||
@@ -55,7 +55,7 @@ func (j *Client) SearchMailboxes(accountIds []AccountId, filter MailboxFilterEle
|
||||
return ZeroResultV[map[AccountId][]Mailbox](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId][]Mailbox, State, Error) {
|
||||
return command(j, Operation("SearchMailboxes"), ctx, cmd, func(body *Response) (map[AccountId][]Mailbox, State, Error) {
|
||||
resp := map[AccountId][]Mailbox{}
|
||||
stateByAccountid := map[AccountId]State{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
@@ -89,7 +89,7 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []AccountId, roles []string,
|
||||
return ZeroResultV[map[AccountId]map[string]string](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]map[string]string, State, Error) {
|
||||
return command(j, Operation("SearchMailboxIdsPerRole"), ctx, cmd, func(body *Response) (map[AccountId]map[string]string, State, Error) {
|
||||
resp := map[AccountId]map[string]string{}
|
||||
stateByAccountid := map[AccountId]State{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
@@ -228,7 +228,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []AccountId, ctx Con
|
||||
return ZeroResultV[map[AccountId]string](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (map[AccountId]string, State, Error) {
|
||||
return command(j, Operation("GetInboxNameForMultipleAccounts"), ctx, cmd, func(body *Response) (map[AccountId]string, State, Error) {
|
||||
resp := make(map[AccountId]string, n)
|
||||
stateByAccountId := make(map[AccountId]State, n)
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
|
||||
@@ -93,7 +93,7 @@ func (j *Client) GetObjects(accountId AccountId, //NOSONAR
|
||||
return ZeroResultV[Objects](), err
|
||||
}
|
||||
|
||||
return command(j, ctx, cmd, func(body *Response) (Objects, State, Error) {
|
||||
return command(j, Operation("GetObjects"), ctx, cmd, func(body *Response) (Objects, State, Error) {
|
||||
objs := Objects{}
|
||||
states := map[string]State{}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func (j *Client) SetVacationResponse(accountId AccountId, vacation VacationRespo
|
||||
if err != nil {
|
||||
return ZeroResultV[VacationResponse](), err
|
||||
}
|
||||
return command(j, ctx, cmd, func(body *Response) (VacationResponse, State, Error) {
|
||||
return command(j, Operation("SetVacationResponse"), ctx, cmd, func(body *Response) (VacationResponse, State, Error) {
|
||||
var setResponse VacationResponseSetResponse
|
||||
err = retrieveSet(ctx, body, set, "0", &setResponse)
|
||||
if err != nil {
|
||||
|
||||
@@ -662,7 +662,7 @@ func createJmapClient(container *testcontainers.DockerContainer, ctx context.Con
|
||||
|
||||
api := NewHttpJmapClient(&jh, auth, eventListener, true, 8192, true, 8192)
|
||||
|
||||
wscf, err := NewHttpWsClientFactory(wsd, auth, logger, eventListener)
|
||||
wscf, err := NewHttpWsClientFactory(wsd, auth, logger, eventListener, true, 8192)
|
||||
if err != nil {
|
||||
return Client{}, nil, nil, err
|
||||
}
|
||||
@@ -676,6 +676,10 @@ type ContextPasswordAuthHttpJmapClientAuthenticator struct {
|
||||
|
||||
var _ HttpJmapClientAuthenticator = &ContextPasswordAuthHttpJmapClientAuthenticator{}
|
||||
|
||||
func (h *ContextPasswordAuthHttpJmapClientAuthenticator) GetId() string {
|
||||
return "context"
|
||||
}
|
||||
|
||||
func (h *ContextPasswordAuthHttpJmapClientAuthenticator) Authenticate(ctx context.Context, username string, _ *oclog.Logger, req *http.Request) Error {
|
||||
password := ctx.Value(h.key).(string)
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
279
pkg/jmap/http.go
279
pkg/jmap/http.go
@@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -42,62 +41,64 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
logEndpoint = "endpoint"
|
||||
logUri = "uri"
|
||||
logMethod = "method"
|
||||
logHttpStatus = "status"
|
||||
logHttpStatusCode = "status-code"
|
||||
logHttpUrl = "url"
|
||||
logProto = "proto"
|
||||
logProtoJmap = "jmap"
|
||||
logProtoJmapWs = "jmapws"
|
||||
logType = "type"
|
||||
logTypeRequest = "request"
|
||||
logTypeResponse = "response"
|
||||
logTypePush = "push"
|
||||
logDuration = "duration"
|
||||
logBodyTruncated = "truncated"
|
||||
logEndpoint = "endpoint"
|
||||
logUri = "uri"
|
||||
logMethod = "method"
|
||||
logHttpStatus = "status"
|
||||
logHttpStatusCode = "status-code"
|
||||
logHttpUrl = "url"
|
||||
logProto = "proto"
|
||||
logProtoJmap = "jmap"
|
||||
logProtoJmapWs = "jmapws"
|
||||
logType = "type"
|
||||
logTypeRequest = "request"
|
||||
logTypeResponse = "response"
|
||||
logTypePush = "push"
|
||||
logDuration = "duration"
|
||||
logBodyTruncated = "truncated"
|
||||
logAuthenticatorId = "auth-id"
|
||||
)
|
||||
|
||||
// Record JMAP HTTP execution events that may occur, e.g. using metrics.
|
||||
type HttpJmapApiClientEventListener interface {
|
||||
OnSuccessfulRequest(endpoint string, status int)
|
||||
OnFailedRequest(endpoint string, err error)
|
||||
OnFailedRequestWithStatus(endpoint string, status int)
|
||||
OnResponseBodyReadingError(endpoint string, err error)
|
||||
OnResponseBodyUnmarshallingError(endpoint string, err error)
|
||||
OnSuccessfulWsRequest(endpoint string, status int)
|
||||
OnFailedWsHandshakeRequestWithStatus(endpoint string, status int)
|
||||
OnSuccessfulRequest(endpoint string, op Operation, status int)
|
||||
OnFailedRequest(endpoint string, op Operation, err error)
|
||||
OnFailedRequestWithStatus(endpoint string, op Operation, status int)
|
||||
OnResponseBodyReadingError(endpoint string, op Operation, err error)
|
||||
OnResponseBodyUnmarshallingError(endpoint string, op Operation, err error)
|
||||
OnSuccessfulWsRequest(endpoint string, op Operation, status int)
|
||||
OnFailedWsHandshakeRequestWithStatus(endpoint string, op Operation, status int)
|
||||
}
|
||||
|
||||
type nullHttpJmapApiClientEventListener struct {
|
||||
}
|
||||
|
||||
func (l nullHttpJmapApiClientEventListener) OnSuccessfulRequest(endpoint string, status int) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnSuccessfulRequest(endpoint string, op Operation, status int) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedRequest(endpoint string, err error) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedRequest(endpoint string, op Operation, err error) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedRequestWithStatus(endpoint string, status int) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedRequestWithStatus(endpoint string, op Operation, status int) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnResponseBodyReadingError(endpoint string, err error) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnResponseBodyReadingError(endpoint string, op Operation, err error) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnResponseBodyUnmarshallingError(endpoint string, err error) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnResponseBodyUnmarshallingError(endpoint string, op Operation, err error) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnSuccessfulWsRequest(endpoint string, status int) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnSuccessfulWsRequest(endpoint string, op Operation, status int) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedWsHandshakeRequestWithStatus(endpoint string, status int) {
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedWsHandshakeRequestWithStatus(endpoint string, op Operation, status int) {
|
||||
// null implementation does nothing
|
||||
}
|
||||
|
||||
var _ HttpJmapApiClientEventListener = nullHttpJmapApiClientEventListener{}
|
||||
|
||||
type HttpJmapClientAuthenticator interface {
|
||||
GetId() string
|
||||
Authenticate(ctx context.Context, username string, logger *log.Logger, req *http.Request) Error
|
||||
AuthenticateWS(ctx context.Context, username string, logger *log.Logger, headers http.Header) Error
|
||||
}
|
||||
@@ -130,6 +131,10 @@ func (h *MasterAuthHttpJmapClientAuthenticator) auth(username string, headers ht
|
||||
headers.Set("Authorization", sb.String())
|
||||
}
|
||||
|
||||
func (h *MasterAuthHttpJmapClientAuthenticator) GetId() string {
|
||||
return "master"
|
||||
}
|
||||
|
||||
func (h *MasterAuthHttpJmapClientAuthenticator) Authenticate(_ context.Context, username string, _ *log.Logger, req *http.Request) Error {
|
||||
h.auth(username, req.Header)
|
||||
return nil
|
||||
@@ -177,8 +182,8 @@ func (e AuthenticationError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (h *HttpJmapClient) auth(ctx context.Context, username string, logger *log.Logger, req *http.Request) Error {
|
||||
return h.authenticator.Authenticate(ctx, username, logger, req)
|
||||
func (h *HttpJmapClient) auth(ctx context.Context, username string, logger *log.Logger, req *http.Request) (string, Error) {
|
||||
return h.authenticator.GetId(), h.authenticator.Authenticate(ctx, username, logger, req)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -188,33 +193,16 @@ var (
|
||||
func (h *HttpJmapClient) beforeRequest(_ context.Context, logger *log.Logger, _ string, endpoint string, req *http.Request) Error {
|
||||
l := logger.Trace()
|
||||
if h.traceRequests && l.Enabled() {
|
||||
var logBuilder bytes.Buffer
|
||||
for key, values := range req.Header {
|
||||
if key == "Authorization" {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
|
||||
if err := dumpHttpRequest(req, h.traceMaxRequestBodySize, func(method string, uri string, content string, truncated bool) {
|
||||
if truncated {
|
||||
l = l.Bool(logBodyTruncated, true)
|
||||
}
|
||||
l.Str(logMethod, req.Method).Str(logUri, req.URL.String()).
|
||||
Str(logEndpoint, endpoint).Str(logProto, logProtoJmap).Str(logType, logTypeRequest).
|
||||
Msg(content)
|
||||
}); err != nil {
|
||||
return jmapError(err, JmapErrorRequestTracing)
|
||||
}
|
||||
if h.traceMaxRequestBodySize >= 0 && req.Body != nil && req.Body != http.NoBody {
|
||||
peekBuffer := new(bytes.Buffer)
|
||||
limitedReader := io.LimitReader(req.Body, h.traceMaxRequestBodySize)
|
||||
tee := io.TeeReader(limitedReader, peekBuffer)
|
||||
if _, err := io.Copy(io.Discard, tee); err != nil {
|
||||
return jmapError(fmt.Errorf("failed to peek at the request body for tracing: %v", err), JmapErrorRequestTracing)
|
||||
} else {
|
||||
logBuilder.Write(peekBuffer.Bytes())
|
||||
if int64(peekBuffer.Len()) >= h.traceMaxRequestBodySize {
|
||||
l = l.Bool(logBodyTruncated, true)
|
||||
}
|
||||
fullBodyReader := io.MultiReader(peekBuffer, req.Body)
|
||||
req.Body = io.NopCloser(fullBodyReader)
|
||||
}
|
||||
}
|
||||
l.Str(logMethod, req.Method).Str(logUri, req.URL.String()).
|
||||
Str(logEndpoint, endpoint).Str(logProto, logProtoJmap).Str(logType, logTypeRequest).
|
||||
Msg(logBuilder.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -234,48 +222,15 @@ func (c httpResponseReadCloser) Close() error {
|
||||
func (h *HttpJmapClient) response(_ context.Context, logger *log.Logger, _ string, endpoint string, duration time.Duration, req *http.Request, resp *http.Response) (io.ReadCloser, error) {
|
||||
l := logger.Trace()
|
||||
if h.traceResponses && l.Enabled() {
|
||||
var logBuilder bytes.Buffer
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
var peekBuffer *bytes.Buffer = nil
|
||||
if h.traceMaxResponseBodySize > 0 {
|
||||
peekBuffer = new(bytes.Buffer)
|
||||
limitedReader := io.LimitReader(resp.Body, h.traceMaxResponseBodySize)
|
||||
tee := io.TeeReader(limitedReader, peekBuffer)
|
||||
if _, err := io.Copy(io.Discard, tee); err != nil {
|
||||
if resp.Body != nil {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if int64(peekBuffer.Len()) >= h.traceMaxResponseBodySize {
|
||||
l = l.Bool(logBodyTruncated, true)
|
||||
}
|
||||
|
||||
logBuilder.Write(peekBuffer.Bytes())
|
||||
}
|
||||
|
||||
l.Str(logProto, logProtoJmap).Str(logType, logTypeResponse).Str(logEndpoint, endpoint).
|
||||
Str(logMethod, req.Method).Str(logUri, req.URL.String()).
|
||||
Str(logHttpStatus, log.SafeString(resp.Status)).Int(logHttpStatusCode, resp.StatusCode).
|
||||
Dur(logDuration, duration).
|
||||
Msg(logBuilder.String())
|
||||
|
||||
if peekBuffer != nil {
|
||||
return httpResponseReadCloser{
|
||||
body: resp.Body,
|
||||
Reader: io.MultiReader(peekBuffer, resp.Body),
|
||||
}, nil
|
||||
} else {
|
||||
return resp.Body, nil
|
||||
}
|
||||
return dumpHttpResponse(req, resp, h.traceMaxResponseBodySize, func(method, uri, content string, truncated bool) {
|
||||
l.Str(logProto, logProtoJmap).Str(logType, logTypeResponse).Str(logEndpoint, endpoint).
|
||||
Str(logMethod, method).Str(logUri, uri).
|
||||
Str(logHttpStatus, log.SafeString(resp.Status)).Int(logHttpStatusCode, resp.StatusCode).
|
||||
Dur(logDuration, duration).
|
||||
Msg(content)
|
||||
}, func(err error) {
|
||||
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
|
||||
})
|
||||
} else {
|
||||
return resp.Body, nil
|
||||
}
|
||||
@@ -309,15 +264,17 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us
|
||||
return SessionResponse{}, err
|
||||
}
|
||||
|
||||
if err := h.auth(ctx, username, logger, req); err != nil {
|
||||
if authenticatorId, err := h.auth(ctx, username, logger, req); err != nil {
|
||||
return SessionResponse{}, err
|
||||
} else {
|
||||
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
res, err := h.client.Do(req)
|
||||
duration := time.Since(before)
|
||||
if err != nil {
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
h.listener.OnFailedRequest(endpoint, Operation("GetSession"), err)
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", sessionUrl)
|
||||
return SessionResponse{}, jmapError(err, JmapErrorInvalidHttpRequest)
|
||||
}
|
||||
@@ -334,29 +291,29 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us
|
||||
}()
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, Operation("GetSession"), res.StatusCode)
|
||||
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 200")
|
||||
return SessionResponse{}, jmapError(fmt.Errorf("JMAP API response status is %v", res.Status), JmapErrorServerResponse)
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
h.listener.OnSuccessfulRequest(endpoint, Operation("GetSession"), res.StatusCode)
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body") //NOSONAR
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
h.listener.OnResponseBodyReadingError(endpoint, Operation("GetSession"), err)
|
||||
return SessionResponse{}, jmapError(err, JmapErrorReadingResponseBody)
|
||||
}
|
||||
|
||||
var data SessionResponse
|
||||
if err := json.NewDecoder(body).Decode(&data); err != nil {
|
||||
logger.Error().Str(logHttpUrl, log.SafeString(sessionUrlStr)).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
|
||||
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
|
||||
h.listener.OnResponseBodyUnmarshallingError(endpoint, Operation("GetSession"), err)
|
||||
return SessionResponse{}, jmapError(err, JmapErrorDecodingResponseBody)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapClient) Command(request Request, ctx Context) (io.ReadCloser, Language, Error) { //NOSONAR
|
||||
func (h *HttpJmapClient) Command(operation Operation, request Request, ctx Context) (io.ReadCloser, Language, Error) { //NOSONAR
|
||||
session := ctx.Session
|
||||
logger := ctx.Logger
|
||||
acceptLanguage := ctx.AcceptLanguage
|
||||
@@ -389,15 +346,17 @@ func (h *HttpJmapClient) Command(request Request, ctx Context) (io.ReadCloser, L
|
||||
|
||||
h.beforeRequest(cotx, logger, session.Username, endpoint, req)
|
||||
|
||||
if err := h.auth(cotx, session.Username, logger, req); err != nil {
|
||||
if authenticatorId, err := h.auth(cotx, session.Username, logger, req); err != nil {
|
||||
return nil, "", err
|
||||
} else {
|
||||
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
res, err := h.client.Do(req)
|
||||
duration := time.Since(before)
|
||||
if err != nil {
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
h.listener.OnFailedRequest(endpoint, operation, err)
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
|
||||
return nil, "", jmapError(err, JmapErrorSendingRequest)
|
||||
}
|
||||
@@ -411,22 +370,22 @@ func (h *HttpJmapClient) Command(request Request, ctx Context) (io.ReadCloser, L
|
||||
language := Language(res.Header.Get("Content-Language")) //NOSONAR
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
h.listener.OnResponseBodyReadingError(endpoint, operation, err)
|
||||
return nil, language, jmapError(err, JmapErrorServerResponse)
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx") //NOSONAR
|
||||
return nil, language, jmapError(err, JmapErrorServerResponse)
|
||||
}
|
||||
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
|
||||
|
||||
return body, language, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapClient) UploadBinary(uploadUrl string, endpoint string, contentType string, body io.Reader, ctx Context) (UploadedBlob, Language, Error) { //NOSONAR
|
||||
func (h *HttpJmapClient) UploadBinary(uploadUrl string, operation Operation, endpoint string, contentType string, body io.Reader, ctx Context) (UploadedBlob, Language, Error) { //NOSONAR
|
||||
session := ctx.Session
|
||||
logger := ctx.Logger
|
||||
acceptLanguage := ctx.AcceptLanguage
|
||||
@@ -446,15 +405,17 @@ func (h *HttpJmapClient) UploadBinary(uploadUrl string, endpoint string, content
|
||||
}
|
||||
h.beforeRequest(cotx, logger, session.Username, endpoint, req)
|
||||
|
||||
if err := h.auth(cotx, session.Username, logger, req); err != nil {
|
||||
if authenticatorId, err := h.auth(cotx, session.Username, logger, req); err != nil {
|
||||
return UploadedBlob{}, "", err
|
||||
} else {
|
||||
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
res, err := h.client.Do(req)
|
||||
duration := time.Since(before)
|
||||
if err != nil {
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
h.listener.OnFailedRequest(endpoint, operation, err)
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl)
|
||||
return UploadedBlob{}, "", jmapError(err, JmapErrorSendingRequest)
|
||||
}
|
||||
@@ -476,28 +437,28 @@ func (h *HttpJmapClient) UploadBinary(uploadUrl string, endpoint string, content
|
||||
language := Language(res.Header.Get("Content-Language"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
h.listener.OnResponseBodyReadingError(endpoint, operation, err)
|
||||
return UploadedBlob{}, language, jmapError(err, JmapErrorServerResponse)
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
|
||||
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
|
||||
return UploadedBlob{}, language, jmapError(err, JmapErrorServerResponse)
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
|
||||
|
||||
var result UploadedBlob
|
||||
if err := json.NewDecoder(responseBody).Decode(&result); err != nil {
|
||||
logger.Error().Str(logHttpUrl, log.SafeString(uploadUrl)).Err(err).Msg("failed to decode JSON payload from the upload response")
|
||||
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
|
||||
h.listener.OnResponseBodyUnmarshallingError(endpoint, operation, err)
|
||||
return UploadedBlob{}, language, jmapError(err, JmapErrorDecodingResponseBody)
|
||||
}
|
||||
|
||||
return result, language, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapClient) DownloadBinary(downloadUrl string, endpoint string, ctx Context) (*BlobDownload, Language, Error) { //NOSONAR
|
||||
func (h *HttpJmapClient) DownloadBinary(downloadUrl string, operation Operation, endpoint string, ctx Context) (*BlobDownload, Language, Error) { //NOSONAR
|
||||
session := ctx.Session
|
||||
logger := ctx.Logger
|
||||
acceptLanguage := ctx.AcceptLanguage
|
||||
@@ -516,15 +477,17 @@ func (h *HttpJmapClient) DownloadBinary(downloadUrl string, endpoint string, ctx
|
||||
}
|
||||
h.beforeRequest(cotx, logger, session.Username, endpoint, req)
|
||||
|
||||
if err := h.auth(cotx, session.Username, logger, req); err != nil {
|
||||
if authenticatorId, err := h.auth(cotx, session.Username, logger, req); err != nil {
|
||||
return nil, "", err
|
||||
} else {
|
||||
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
res, err := h.client.Do(req)
|
||||
duration := time.Since(before)
|
||||
if err != nil {
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
h.listener.OnFailedRequest(endpoint, operation, err)
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl)
|
||||
return nil, "", jmapError(err, JmapErrorSendingRequest)
|
||||
}
|
||||
@@ -539,7 +502,7 @@ func (h *HttpJmapClient) DownloadBinary(downloadUrl string, endpoint string, ctx
|
||||
language := Language(res.Header.Get("Content-Language"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
h.listener.OnResponseBodyReadingError(endpoint, operation, err)
|
||||
return nil, language, jmapError(err, JmapErrorServerResponse)
|
||||
}
|
||||
|
||||
@@ -547,11 +510,11 @@ func (h *HttpJmapClient) DownloadBinary(downloadUrl string, endpoint string, ctx
|
||||
return nil, language, nil
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
|
||||
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
|
||||
return nil, language, jmapError(err, JmapErrorServerResponse)
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
|
||||
|
||||
sizeStr := res.Header.Get("Content-Length")
|
||||
size := -1
|
||||
@@ -604,16 +567,20 @@ type WebSocketPushDisable struct {
|
||||
}
|
||||
|
||||
type HttpWsClientFactory struct {
|
||||
dialer *websocket.Dialer
|
||||
authenticator HttpJmapClientAuthenticator
|
||||
logger *log.Logger
|
||||
eventListener HttpJmapApiClientEventListener
|
||||
dialer *websocket.Dialer
|
||||
authenticator HttpJmapClientAuthenticator
|
||||
logger *log.Logger
|
||||
eventListener HttpJmapApiClientEventListener
|
||||
traceWsResponses bool
|
||||
traceMaxResponseBodySize int64
|
||||
}
|
||||
|
||||
var _ WsClientFactory = &HttpWsClientFactory{}
|
||||
|
||||
func NewHttpWsClientFactory(dialer *websocket.Dialer, authenticator HttpJmapClientAuthenticator, logger *log.Logger,
|
||||
eventListener HttpJmapApiClientEventListener) (*HttpWsClientFactory, error) {
|
||||
eventListener HttpJmapApiClientEventListener,
|
||||
traceWsResponses bool, traceMaxResponseBodySize int64,
|
||||
) (*HttpWsClientFactory, error) {
|
||||
// RFC 8887: Section 4.2:
|
||||
// Otherwise, the client MUST make an authenticated HTTP request [RFC7235] on the encrypted connection
|
||||
// and MUST include the value "jmap" in the list of protocols for the "Sec-WebSocket-Protocol" header
|
||||
@@ -621,10 +588,12 @@ func NewHttpWsClientFactory(dialer *websocket.Dialer, authenticator HttpJmapClie
|
||||
dialer.Subprotocols = []string{"jmap"}
|
||||
|
||||
return &HttpWsClientFactory{
|
||||
dialer: dialer,
|
||||
authenticator: authenticator,
|
||||
logger: logger,
|
||||
eventListener: eventListener,
|
||||
dialer: dialer,
|
||||
authenticator: authenticator,
|
||||
logger: logger,
|
||||
eventListener: eventListener,
|
||||
traceWsResponses: true,
|
||||
traceMaxResponseBodySize: 4 * 1024,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -632,9 +601,7 @@ func (w *HttpWsClientFactory) auth(ctx context.Context, username string, logger
|
||||
return w.authenticator.AuthenticateWS(ctx, username, logger, h)
|
||||
}
|
||||
|
||||
func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func() (*Session, error)) (*websocket.Conn, string, string, Error) {
|
||||
logger := w.logger
|
||||
|
||||
func (w *HttpWsClientFactory) connect(operation Operation, ctx context.Context, sessionProvider func() (*Session, error)) (*websocket.Conn, string, string, Error) {
|
||||
session, err := sessionProvider()
|
||||
if err != nil {
|
||||
return nil, "", "", jmapError(err, JmapErrorWssFailedToRetrieveSession)
|
||||
@@ -651,29 +618,49 @@ func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func(
|
||||
u := session.WebsocketUrl
|
||||
endpoint := session.WebsocketEndpoint
|
||||
|
||||
logger := log.From(w.logger.With().Str("username", log.SafeString(username)).Str("url", log.SafeString(u.String())))
|
||||
|
||||
h := http.Header{}
|
||||
w.auth(ctx, username, logger, h)
|
||||
w.logger.Trace().Str("username", log.SafeString(username)).Str("url", log.SafeString(u.String())).Msgf("connecting")
|
||||
w.logger.Trace().Str("username", log.SafeString(username)).Str("url", log.SafeString(u.String())).Msgf("connecting") // TODO more/better attributes here for WS connection attempts
|
||||
before := time.Now()
|
||||
c, res, err := w.dialer.DialContext(ctx, u.String(), h)
|
||||
duration := time.Since(before)
|
||||
if err != nil {
|
||||
return nil, "", endpoint, jmapError(err, JmapErrorFailedToEstablishWssConnection)
|
||||
}
|
||||
|
||||
if w.logger.Trace().Enabled() {
|
||||
responseBytes, err := httputil.DumpResponse(res, true)
|
||||
if err == nil {
|
||||
logger.Trace().Str(logEndpoint, endpoint).Str(logProto, logProtoJmapWs).Str(logType, logTypeResponse).
|
||||
Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).
|
||||
Msg(string(responseBytes))
|
||||
var body io.ReadCloser = nil
|
||||
{
|
||||
l := logger.Trace()
|
||||
if w.traceWsResponses && l.Enabled() {
|
||||
body, err = dumpHttpResponse(nil, res, w.traceMaxResponseBodySize, func(method, uri, content string, truncated bool) {
|
||||
l.Str(logProto, logProtoJmap).Str(logType, logTypeResponse).Str(logEndpoint, endpoint).
|
||||
Str(logMethod, method).Str(logUri, uri).
|
||||
Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).
|
||||
Dur(logDuration, duration).
|
||||
Msg(content)
|
||||
}, func(err error) {
|
||||
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// we are not using the response body at all, at least for now
|
||||
defer func() {
|
||||
if body != nil {
|
||||
if err := body.Close(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if res.StatusCode != 101 {
|
||||
w.eventListener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
w.eventListener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
|
||||
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 101")
|
||||
return nil, "", endpoint, jmapError(fmt.Errorf("JMAP WS API response status is %v", res.Status), JmapErrorServerResponse)
|
||||
} else {
|
||||
w.eventListener.OnSuccessfulWsRequest(endpoint, res.StatusCode)
|
||||
w.eventListener.OnSuccessfulWsRequest(endpoint, operation, res.StatusCode)
|
||||
}
|
||||
|
||||
// RFC 8887: Section 4.2:
|
||||
@@ -748,7 +735,7 @@ func (w *HttpWsClient) readPump() { //NOSONAR
|
||||
}
|
||||
|
||||
func (w *HttpWsClientFactory) EnableNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
|
||||
c, username, endpoint, jerr := w.connect(ctx, sessionProvider)
|
||||
c, username, endpoint, jerr := w.connect(Operation("EnableNotifications"), ctx, sessionProvider)
|
||||
if jerr != nil {
|
||||
return nil, jerr
|
||||
}
|
||||
|
||||
@@ -1243,11 +1243,11 @@ type Response struct {
|
||||
// Clients may use this to detect if this object has changed and needs to be refetched.
|
||||
SessionState SessionState `json:"sessionState"`
|
||||
|
||||
// This MUST be the string "Response".
|
||||
// Only for Websockets: This MUST be the string "Response".
|
||||
// The specification extends the Response object with two additional arguments when used over a WebSocket.
|
||||
Type TypeOfResponse `json:"@type,omitempty"`
|
||||
|
||||
// MUST be returned if an identifier is included in the request (optional).
|
||||
// Only for Websockets: MUST be returned if an identifier is included in the request (optional).
|
||||
RequestId string `json:"requestId,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], ID any, RESP any](
|
||||
return ZeroResultV[RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
var response GETRESP
|
||||
err = retrieveGet(ctx, body, get, "0", &response)
|
||||
if err != nil {
|
||||
@@ -73,7 +73,7 @@ func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], ID any,
|
||||
return ZeroResultV[RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
result := map[AccountId]ITEM{}
|
||||
responses := map[AccountId]GETRESP{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
@@ -112,7 +112,7 @@ func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP Se
|
||||
return ZeroResultV[*T](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (*T, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (*T, State, Error) {
|
||||
var setResponse SETRESP
|
||||
err = retrieveSet(ctx, body, set, "0", &setResponse)
|
||||
if err != nil {
|
||||
@@ -165,7 +165,7 @@ func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name
|
||||
return ZeroResultV[map[string]SetError](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (map[string]SetError, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (map[string]SetError, State, Error) {
|
||||
var setResponse RESP
|
||||
err = retrieveSet(ctx, body, set, "0", &setResponse)
|
||||
if err != nil {
|
||||
@@ -213,7 +213,7 @@ func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
|
||||
return ZeroResultV[RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
var changesResponse CHANGESRESP
|
||||
err = retrieveChanges(ctx, body, changes, "0", &changesResponse)
|
||||
if err != nil {
|
||||
@@ -302,7 +302,7 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
|
||||
return ZeroResultV[RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
changesItemByAccount := make(map[AccountId]CHANGESITEM, n)
|
||||
stateByAccountId := make(map[AccountId]State, n)
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
@@ -357,7 +357,7 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
|
||||
return ZeroResultV[RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
var changesResponse CHANGESRESP
|
||||
err = retrieveChanges(ctx, body, changes, "0", &changesResponse)
|
||||
if err != nil {
|
||||
@@ -405,7 +405,7 @@ func update[T Foo, CHANGES Change[T], SET SetCommand[T], GET GetCommand[T], RESP
|
||||
return ZeroResultV[RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (RESP, State, Error) {
|
||||
var setResponse SETRESP
|
||||
err = retrieveSet(ctx, body, update, "0", &setResponse)
|
||||
if err != nil {
|
||||
@@ -529,7 +529,7 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T /*, QUERY*/], GET
|
||||
return ZeroResultV[map[AccountId]*RESP](), err
|
||||
}
|
||||
|
||||
return command(client, ctx, cmd, func(body *Response) (map[AccountId]*RESP, State, Error) {
|
||||
return command(client, Operation(name), ctx, cmd, func(body *Response) (map[AccountId]*RESP, State, Error) {
|
||||
resp := map[AccountId]*RESP{}
|
||||
stateByAccountId := map[AccountId]State{}
|
||||
for accountId, queryParams := range accountIds {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -62,7 +65,10 @@ type Cmdr interface {
|
||||
Hooks
|
||||
}
|
||||
|
||||
type Operation string
|
||||
|
||||
func command[T any](client Cmdr, //NOSONAR
|
||||
operation Operation,
|
||||
ctx Context,
|
||||
request Request,
|
||||
mapper func(body *Response) (T, State, Error)) (Result[T], Error) {
|
||||
@@ -70,7 +76,7 @@ func command[T any](client Cmdr, //NOSONAR
|
||||
logger := ctx.Logger
|
||||
|
||||
before := time.Now()
|
||||
responseBody, language, jmapErr := client.Api().Command(request, ctx)
|
||||
responseBody, language, jmapErr := client.Api().Command(operation, request, ctx)
|
||||
duration := time.Since(before)
|
||||
if jmapErr != nil {
|
||||
return ZeroResult[T](single(duration)), jmapErr
|
||||
@@ -445,3 +451,81 @@ func ns(namespaces ...JmapNamespace) []JmapNamespace {
|
||||
func ptr[T any | int | uint | bool | string](t T) *T {
|
||||
return &t
|
||||
}
|
||||
|
||||
func dumpHttpRequest(req *http.Request, maxBodySize int64, closure func(method string, uri string, content string, truncated bool)) error {
|
||||
var logBuilder bytes.Buffer
|
||||
for key, values := range req.Header {
|
||||
if key == "Authorization" {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
truncated := false
|
||||
if maxBodySize >= 0 && req.Body != nil && req.Body != http.NoBody {
|
||||
peekBuffer := new(bytes.Buffer)
|
||||
limitedReader := io.LimitReader(req.Body, maxBodySize)
|
||||
tee := io.TeeReader(limitedReader, peekBuffer)
|
||||
if _, err := io.Copy(io.Discard, tee); err != nil {
|
||||
return fmt.Errorf("failed to peek at the request body for tracing: %v", err)
|
||||
} else {
|
||||
logBuilder.Write(peekBuffer.Bytes())
|
||||
if int64(peekBuffer.Len()) >= maxBodySize {
|
||||
truncated = true
|
||||
}
|
||||
fullBodyReader := io.MultiReader(peekBuffer, req.Body)
|
||||
req.Body = io.NopCloser(fullBodyReader)
|
||||
}
|
||||
}
|
||||
closure(req.Method, req.URL.String(), logBuilder.String(), truncated)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpHttpResponse(req *http.Request, resp *http.Response, maxBodySize int64,
|
||||
closure func(method string, uri string, content string, truncated bool),
|
||||
closeErrorClosure func(err error)) (io.ReadCloser, error) {
|
||||
var logBuilder bytes.Buffer
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
var peekBuffer *bytes.Buffer = nil
|
||||
truncated := false
|
||||
if maxBodySize > 0 {
|
||||
peekBuffer = new(bytes.Buffer)
|
||||
limitedReader := io.LimitReader(resp.Body, maxBodySize)
|
||||
tee := io.TeeReader(limitedReader, peekBuffer)
|
||||
if _, err := io.Copy(io.Discard, tee); err != nil {
|
||||
if resp.Body != nil {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
closeErrorClosure(err)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if int64(peekBuffer.Len()) >= maxBodySize {
|
||||
truncated = true
|
||||
}
|
||||
|
||||
logBuilder.Write(peekBuffer.Bytes())
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
closure(req.Method, req.URL.String(), logBuilder.String(), truncated)
|
||||
} else {
|
||||
closure("", "", logBuilder.String(), truncated)
|
||||
}
|
||||
|
||||
if peekBuffer != nil {
|
||||
return httpResponseReadCloser{
|
||||
body: resp.Body,
|
||||
Reader: io.MultiReader(peekBuffer, resp.Body),
|
||||
}, nil
|
||||
} else {
|
||||
return resp.Body, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,14 @@ func MapMap[A comparable, B any, X comparable, Y any](m map[A]B, mapper func(A,
|
||||
return r
|
||||
}
|
||||
|
||||
func FlatMap[K comparable, V any, E any](m map[K]V, mapper func(K, V) E) []E {
|
||||
r := make([]E, len(m))
|
||||
for k, v := range m {
|
||||
r = append(r, mapper(k, v))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Creates a map from a map, keeping each key as-is, and using the mapper
|
||||
// function to determine the value to store into the resulting map.
|
||||
func MapValues[K comparable, S any, T any](m map[K]S, mapper func(S) T) map[K]T {
|
||||
|
||||
@@ -156,25 +156,26 @@ type groupwareHttpJmapApiClientMetricsRecorder struct {
|
||||
|
||||
var _ jmap.HttpJmapApiClientEventListener = groupwareHttpJmapApiClientMetricsRecorder{}
|
||||
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnSuccessfulRequest(endpoint string, status int) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnSuccessfulRequest(endpoint string, op jmap.Operation, status int) {
|
||||
r.m.SuccessfulRequestPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
r.m.OperationSpreadCounter.With(metrics.EndpointAndOperation(endpoint, op)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedRequest(endpoint string, err error) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedRequest(endpoint string, op jmap.Operation, err error) {
|
||||
r.m.FailedRequestPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedRequestWithStatus(endpoint string, status int) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedRequestWithStatus(endpoint string, op jmap.Operation, status int) {
|
||||
r.m.FailedRequestStatusPerEndpointCounter.With(metrics.EndpointAndStatus(endpoint, status)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnResponseBodyReadingError(endpoint string, err error) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnResponseBodyReadingError(endpoint string, op jmap.Operation, err error) {
|
||||
r.m.ResponseBodyReadingErrorPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnResponseBodyUnmarshallingError(endpoint string, err error) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnResponseBodyUnmarshallingError(endpoint string, op jmap.Operation, err error) {
|
||||
r.m.ResponseBodyUnmarshallingErrorPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnSuccessfulWsRequest(endpoint string, status int) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnSuccessfulWsRequest(endpoint string, op jmap.Operation, status int) {
|
||||
// TODO metrics for WSS
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedWsHandshakeRequestWithStatus(endpoint string, status int) {
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedWsHandshakeRequestWithStatus(endpoint string, op jmap.Operation, status int) {
|
||||
// TODO metrics for WSS
|
||||
}
|
||||
|
||||
@@ -210,7 +211,10 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
sanitize := true // TODO make configurable
|
||||
|
||||
m := metrics.New(prometheusRegistry, logger)
|
||||
m, err := metrics.New(prometheusRegistry, logger)
|
||||
if err != nil {
|
||||
return nil, GroupwareInitializationError{Message: fmt.Sprintf("failed to configure metrics: %v", err), Err: err}
|
||||
}
|
||||
|
||||
userProvider := newRevaContextUsernameProvider()
|
||||
|
||||
@@ -269,7 +273,9 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
|
||||
}
|
||||
|
||||
wsf, err = jmap.NewHttpWsClientFactory(wsDialer, auth, logger, jmapMetricsAdapter)
|
||||
wsf, err = jmap.NewHttpWsClientFactory(wsDialer, auth, logger, jmapMetricsAdapter,
|
||||
config.HTTP.TraceResponses, config.HTTP.TraceMaxResponseBodySize,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to create websocket client")
|
||||
return nil, GroupwareInitializationError{Message: "failed to create websocket client", Err: err}
|
||||
|
||||
@@ -89,6 +89,10 @@ var tokenMissingInRevaContext = RevaError{
|
||||
err: errors.New("token is missing from Reva context"),
|
||||
}
|
||||
|
||||
func (h *RevaBearerHttpJmapClientAuthenticator) GetId() string {
|
||||
return "reva"
|
||||
}
|
||||
|
||||
func (h *RevaBearerHttpJmapClientAuthenticator) Authenticate(ctx context.Context, _ string, logger *log.Logger, req *http.Request) jmap.Error {
|
||||
token, ok := revactx.ContextGetToken(ctx)
|
||||
if !ok {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -34,6 +38,7 @@ type Metrics struct {
|
||||
SSEEventsCounter *prometheus.CounterVec
|
||||
OutdatedSessionsCounter prometheus.Counter
|
||||
|
||||
OperationSpreadCounter *prometheus.CounterVec
|
||||
SuccessfulRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestStatusPerEndpointCounter *prometheus.CounterVec
|
||||
@@ -48,6 +53,7 @@ type Metrics struct {
|
||||
var Labels = struct {
|
||||
Endpoint string
|
||||
Result string
|
||||
Operation string
|
||||
SessionCacheType string
|
||||
RequestId string
|
||||
TraceId string
|
||||
@@ -57,6 +63,7 @@ var Labels = struct {
|
||||
}{
|
||||
Endpoint: "endpoint",
|
||||
Result: "result",
|
||||
Operation: "op",
|
||||
SessionCacheType: "type",
|
||||
RequestId: "requestID",
|
||||
TraceId: "traceID",
|
||||
@@ -104,7 +111,7 @@ var Values = struct {
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New(registerer prometheus.Registerer, logger *log.Logger) *Metrics {
|
||||
func New(registerer prometheus.Registerer, logger *log.Logger) (*Metrics, error) {
|
||||
m := &Metrics{
|
||||
SessionCacheDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, Subsystem, "session_cache"),
|
||||
@@ -178,11 +185,17 @@ func New(registerer prometheus.Registerer, logger *log.Logger) *Metrics {
|
||||
Name: "jmap_errors_count",
|
||||
Help: "Number of JMAP errors that occured",
|
||||
}, []string{Labels.Endpoint, Labels.ErrorCode}),
|
||||
OperationSpreadCounter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "jmap_operations",
|
||||
Help: "Spread of operations by their names",
|
||||
}, []string{Labels.Endpoint, Labels.Operation}),
|
||||
SuccessfulRequestPerEndpointCounter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "jmap_requests_count",
|
||||
Help: "Number of JMAP requests",
|
||||
Help: "Number of JMAP requests", //NOSONAR
|
||||
ConstLabels: prometheus.Labels{
|
||||
Labels.Result: Values.Result.Success,
|
||||
},
|
||||
@@ -191,7 +204,7 @@ func New(registerer prometheus.Registerer, logger *log.Logger) *Metrics {
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "jmap_requests_count",
|
||||
Help: "Number of JMAP requests",
|
||||
Help: "Number of JMAP requests", //NOSONAR
|
||||
ConstLabels: prometheus.Labels{
|
||||
Labels.Result: Values.Result.Failure,
|
||||
},
|
||||
@@ -249,40 +262,51 @@ func New(registerer prometheus.Registerer, logger *log.Logger) *Metrics {
|
||||
}, []string{Labels.Endpoint}),
|
||||
}
|
||||
|
||||
registerAll(registerer, m, logger)
|
||||
err := registerAll(registerer, m, logger)
|
||||
|
||||
return m
|
||||
return m, err
|
||||
}
|
||||
|
||||
func WithExemplar(obs prometheus.Observer, value float64, requestId string, traceId string) {
|
||||
obs.(prometheus.ExemplarObserver).ObserveWithExemplar(value, prometheus.Labels{Labels.RequestId: requestId, Labels.TraceId: traceId})
|
||||
}
|
||||
|
||||
func registerAll(registerer prometheus.Registerer, m any, logger *log.Logger) {
|
||||
func registerAll(registerer prometheus.Registerer, m any, logger *log.Logger) error {
|
||||
r := reflect.ValueOf(m)
|
||||
if r.Kind() == reflect.Pointer {
|
||||
r = r.Elem()
|
||||
}
|
||||
total := 0
|
||||
succeeded := 0
|
||||
failed := 0
|
||||
succeeded := []string{}
|
||||
failed := map[string]error{}
|
||||
for i := 0; i < r.NumField(); i++ {
|
||||
n := r.Type().Field(i).Name
|
||||
f := r.Field(i)
|
||||
v := f.Interface()
|
||||
c, ok := v.(prometheus.Collector)
|
||||
if ok {
|
||||
switch c := v.(type) {
|
||||
case prometheus.Collector:
|
||||
total++
|
||||
err := registerer.Register(c)
|
||||
if err != nil {
|
||||
failed++
|
||||
logger.Warn().Err(err).Msgf("failed to register metric '%s' (%T)", n, c)
|
||||
if err := registerer.Register(c); err != nil {
|
||||
failed[n] = err
|
||||
} else {
|
||||
succeeded++
|
||||
succeeded = append(succeeded, n)
|
||||
}
|
||||
case *prometheus.Desc, prometheus.GaugeOpts, prometheus.CounterOpts, prometheus.UntypedOpts:
|
||||
// skip these
|
||||
default:
|
||||
failed[n] = fmt.Errorf("unsupported metric '%s' of type %T", n, c)
|
||||
}
|
||||
}
|
||||
logger.Debug().Msgf("registered %d/%d metrics successfully (%d failed)", succeeded, total, failed)
|
||||
if len(failed) > 0 {
|
||||
msg := strings.Join(structs.FlatMap(failed, func(name string, err error) string {
|
||||
return fmt.Sprintf("'%s' (%v)", name, err)
|
||||
}), ", ")
|
||||
logger.Warn().Msgf("registered %d/%d metrics successfully (%d failed): %s", len(succeeded), total, len(failed), msg)
|
||||
return fmt.Errorf("failed to register metrics: %s", msg)
|
||||
} else {
|
||||
logger.Debug().Msgf("registered %d/%d metrics successfully (%d failed)", len(succeeded), total, len(failed))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type ConstMetricCollector struct {
|
||||
@@ -343,3 +367,7 @@ func Endpoint(endpoint string) prometheus.Labels {
|
||||
func EndpointAndStatus(endpoint string, status int) prometheus.Labels {
|
||||
return prometheus.Labels{Labels.Endpoint: endpoint, Labels.HttpStatusCode: strconv.Itoa(status)}
|
||||
}
|
||||
|
||||
func EndpointAndOperation(endpoint string, operation jmap.Operation) prometheus.Labels {
|
||||
return prometheus.Labels{Labels.Endpoint: endpoint, Labels.Operation: string(operation)}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user