diff --git a/pkg/jmap/api.go b/pkg/jmap/api.go index b5910057e4..3d28996f05 100644 --- a/pkg/jmap/api.go +++ b/pkg/jmap/api.go @@ -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 } diff --git a/pkg/jmap/api_blob.go b/pkg/jmap/api_blob.go index 8d31d1e405..f772da217b 100644 --- a/pkg/jmap/api_blob.go +++ b/pkg/jmap/api_blob.go @@ -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 { diff --git a/pkg/jmap/api_bootstrap.go b/pkg/jmap/api_bootstrap.go index b8d65be06c..3a0de312a4 100644 --- a/pkg/jmap/api_bootstrap.go +++ b/pkg/jmap/api_bootstrap.go @@ -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{} diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go index 6516231a8b..5f52b0919c 100644 --- a/pkg/jmap/api_calendar.go +++ b/pkg/jmap/api_calendar.go @@ -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 { diff --git a/pkg/jmap/api_changes.go b/pkg/jmap/api_changes.go index 7e868f0751..3ebed418ba 100644 --- a/pkg/jmap/api_changes.go +++ b/pkg/jmap/api_changes.go @@ -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, } diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index 2c588960bf..9860fdaedd 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -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 diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go index a778373c93..13ef9cc17c 100644 --- a/pkg/jmap/api_identity.go +++ b/pkg/jmap/api_identity.go @@ -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{} diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 2096da3f4e..9049c66445 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -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 { diff --git a/pkg/jmap/api_objects.go b/pkg/jmap/api_objects.go index d08b1beaac..5e863fa452 100644 --- a/pkg/jmap/api_objects.go +++ b/pkg/jmap/api_objects.go @@ -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{} diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go index c8146f83c6..792fa67704 100644 --- a/pkg/jmap/api_vacation.go +++ b/pkg/jmap/api_vacation.go @@ -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 { diff --git a/pkg/jmap/export_integration_test.go b/pkg/jmap/export_integration_test.go index 83bed572b4..49cb392980 100644 --- a/pkg/jmap/export_integration_test.go +++ b/pkg/jmap/export_integration_test.go @@ -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) diff --git a/pkg/jmap/http.go b/pkg/jmap/http.go index aeecfd2d28..11937d9d3c 100644 --- a/pkg/jmap/http.go +++ b/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 } diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index 3c617a802b..d814ac4b7f 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -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"` } diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index 50e921a55b..49d2c513c7 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -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 { diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index ffa7ced3f8..0341433aff 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -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 + } +} diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 7f7d1c00e8..f2245f8bb8 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -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 { diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go index 45c5e750aa..099e583b0b 100644 --- a/services/groupware/pkg/groupware/framework.go +++ b/services/groupware/pkg/groupware/framework.go @@ -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} diff --git a/services/groupware/pkg/groupware/reva.go b/services/groupware/pkg/groupware/reva.go index d7f3ef7f3c..bd8e224c8c 100644 --- a/services/groupware/pkg/groupware/reva.go +++ b/services/groupware/pkg/groupware/reva.go @@ -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 { diff --git a/services/groupware/pkg/metrics/metrics.go b/services/groupware/pkg/metrics/metrics.go index 08828efdb7..af21a1c28d 100644 --- a/services/groupware/pkg/metrics/metrics.go +++ b/services/groupware/pkg/metrics/metrics.go @@ -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)} +}