Skip to content

Commit d8f9e35

Browse files
RIT3shSapatabbrksCopilot
authored
[3.3.3 backport] CBG-5116: Diagnostic API Improvements (#7999)
Co-authored-by: Ben Brooks <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 2c76b53 commit d8f9e35

22 files changed

+1844
-342
lines changed

base/audit_events.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ const (
3030
AuditIDSyncGatewayStartup AuditID = 53260
3131

3232
// API events
33-
AuditIDPublicHTTPAPIRequest AuditID = 53270
34-
AuditIDAdminHTTPAPIRequest AuditID = 53271
35-
AuditIDMetricsHTTPAPIRequest AuditID = 53272
33+
AuditIDPublicHTTPAPIRequest AuditID = 53270
34+
AuditIDAdminHTTPAPIRequest AuditID = 53271
35+
AuditIDMetricsHTTPAPIRequest AuditID = 53272
36+
AuditIDDiagnosticsHTTPAPIRequest AuditID = 53273
3637

3738
// Auth (public) events
3839
AuditIDPublicUserAuthenticated AuditID = 53280
@@ -266,6 +267,29 @@ var AuditEvents = events{
266267
EventType: eventTypeAdmin,
267268
IsGlobalEvent: true,
268269
},
270+
AuditIDDiagnosticsHTTPAPIRequest: {
271+
Name: "Diagnostic HTTP API request",
272+
Description: "Diagnostic HTTP API request was made",
273+
MandatoryFields: AuditFields{
274+
AuditFieldHTTPMethod: "GET, POST, etc.",
275+
AuditFieldHTTPPath: "request_path",
276+
AuditFieldHTTPStatus: 200,
277+
},
278+
OptionalFields: AuditFields{
279+
AuditFieldRequestBody: "request_body",
280+
AuditFieldRealUserID: map[string]any{
281+
AuditFieldRealUserIDDomain: "user domain",
282+
AuditFieldRealUserIDUser: "user name",
283+
},
284+
},
285+
mandatoryFieldGroups: []fieldGroup{
286+
fieldGroupRequest,
287+
},
288+
EnabledByDefault: false,
289+
FilteringPermitted: true,
290+
EventType: eventTypeAdmin,
291+
IsGlobalEvent: true,
292+
},
269293
AuditIDPublicUserAuthenticated: {
270294
Name: "Public API user authenticated",
271295
Description: "Public API user successfully authenticated",

base/error.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,53 @@ func (me *MultiError) ErrorOrNil() error {
361361
}
362362
return me
363363
}
364+
365+
const syncFnDryRunErrorPrefix = "Error returned from Sync Function"
366+
367+
// SyncFnDryRunError is returned when the sync function dry run returns an error.
368+
// It wraps the original error for errors.Is and the type supports errors.As
369+
type SyncFnDryRunError struct {
370+
Err error
371+
}
372+
373+
func (e *SyncFnDryRunError) Error() string {
374+
if e == nil {
375+
return syncFnDryRunErrorPrefix
376+
}
377+
if e.Err == nil {
378+
return syncFnDryRunErrorPrefix
379+
}
380+
return syncFnDryRunErrorPrefix + ": " + e.Err.Error()
381+
}
382+
383+
func (e *SyncFnDryRunError) Unwrap() error {
384+
if e == nil {
385+
return nil
386+
}
387+
return e.Err
388+
}
389+
390+
const importFilterErrorPrefix = "Error returned from Import Filter"
391+
392+
// ImportFilterError is returned when the import filter dry run returns an error.
393+
// It wraps the original error for errors.Is and the type supports errors.As
394+
type ImportFilterDryRunError struct {
395+
Err error
396+
}
397+
398+
func (e *ImportFilterDryRunError) Error() string {
399+
if e == nil {
400+
return importFilterErrorPrefix
401+
}
402+
if e.Err == nil {
403+
return importFilterErrorPrefix
404+
}
405+
return importFilterErrorPrefix + ": " + e.Err.Error()
406+
}
407+
408+
func (e *ImportFilterDryRunError) Unwrap() error {
409+
if e == nil {
410+
return nil
411+
}
412+
return e.Err
413+
}

channels/sync_runner.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,10 @@ type SyncRunner struct {
116116
expiry *uint32 // document expiry (in seconds) specified via expiry() callback
117117
}
118118

119-
func NewSyncRunner(ctx context.Context, funcSource string, timeout time.Duration) (*SyncRunner, error) {
119+
func NewSyncRunnerWithLogging(ctx context.Context, funcSource string, timeout time.Duration, errorLogFunc, infoLogFunc func(string)) (*SyncRunner, error) {
120120
funcSource = wrappedFuncSource(funcSource)
121121
runner := &SyncRunner{}
122-
err := runner.InitWithLogging(funcSource, timeout,
123-
func(s string) { base.ErrorfCtx(ctx, base.KeyJavascript.String()+": Sync %s", base.UD(s)) },
124-
func(s string) { base.InfofCtx(ctx, base.KeyJavascript, "Sync %s", base.UD(s)) })
122+
err := runner.InitWithLogging(funcSource, timeout, errorLogFunc, infoLogFunc)
125123
if err != nil {
126124
return nil, err
127125
}
@@ -212,6 +210,12 @@ func NewSyncRunner(ctx context.Context, funcSource string, timeout time.Duration
212210
return runner, nil
213211
}
214212

213+
func NewSyncRunner(ctx context.Context, funcSource string, timeout time.Duration) (*SyncRunner, error) {
214+
errorLogFunc := func(s string) { base.ErrorfCtx(ctx, base.KeyJavascript.String()+": Sync %s", base.UD(s)) }
215+
infoLogFunc := func(s string) { base.InfofCtx(ctx, base.KeyJavascript, "Sync %s", base.UD(s)) }
216+
return NewSyncRunnerWithLogging(ctx, funcSource, timeout, errorLogFunc, infoLogFunc)
217+
}
218+
215219
func (runner *SyncRunner) SetFunction(funcSource string) (bool, error) {
216220
funcSource = wrappedFuncSource(funcSource)
217221
return runner.JSRunner.SetFunction(funcSource)

db/change_cache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ func (c *changeCache) DocChanged(event sgbucket.FeedEvent, docType DocumentType)
376376
}
377377

378378
// First unmarshal the doc (just its metadata, to save time/memory):
379-
syncData, rawBody, rawXattrs, err := UnmarshalDocumentSyncDataFromFeed(docJSON, event.DataType, collection.userXattrKey(), false)
379+
syncData, rawBody, rawXattrs, err := UnmarshalDocumentSyncDataFromFeed(docJSON, event.DataType, collection.UserXattrKey(), false)
380380
if err != nil {
381381
// Avoid log noise related to failed unmarshaling of binary documents.
382382
if event.DataType != base.MemcachedDataTypeRaw {
@@ -389,7 +389,7 @@ func (c *changeCache) DocChanged(event sgbucket.FeedEvent, docType DocumentType)
389389
}
390390

391391
// If using xattrs and this isn't an SG write, we shouldn't attempt to cache.
392-
rawUserXattr := rawXattrs[collection.userXattrKey()]
392+
rawUserXattr := rawXattrs[collection.UserXattrKey()]
393393
if collection.UseXattrs() {
394394
if syncData == nil {
395395
return

db/crud.go

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ func (db *DatabaseCollectionWithUser) Put(ctx context.Context, docid string, bod
958958
}
959959

960960
// Make up a new _rev, and add it to the history:
961-
bodyWithoutInternalProps, wasStripped := stripInternalProperties(body)
961+
bodyWithoutInternalProps, wasStripped := StripInternalProperties(body)
962962
canonicalBytesForRevID, err := base.JSONMarshalCanonical(bodyWithoutInternalProps)
963963
if err != nil {
964964
return nil, nil, false, nil, err
@@ -1174,87 +1174,47 @@ func (db *DatabaseCollectionWithUser) PutExistingRevWithBody(ctx context.Context
11741174

11751175
}
11761176

1177-
// SyncFnDryrun Runs a document through the sync function and returns expiry, channels doc was placed in, access map for users, roles, handler errors and sync fn exceptions
1178-
func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, body Body, docID string) (*channels.ChannelMapperOutput, error, error) {
1179-
doc := &Document{
1180-
ID: docID,
1181-
_body: body,
1182-
}
1183-
oldDoc := doc
1184-
if docID != "" {
1185-
if docInBucket, err := db.GetDocument(ctx, docID, DocUnmarshalAll); err == nil {
1186-
oldDoc = docInBucket
1187-
if doc._body == nil {
1188-
body = oldDoc.Body(ctx)
1189-
doc._body = body
1190-
// If no body is given, use doc in bucket as doc with no old doc
1191-
oldDoc._body = nil
1192-
}
1193-
doc._body[BodyRev] = oldDoc.SyncData.CurrentRev
1194-
} else {
1195-
return nil, err, nil
1196-
}
1197-
} else {
1198-
oldDoc._body = nil
1199-
}
1200-
1201-
delete(body, BodyId)
1202-
1203-
// Get the revision ID to match, and the new generation number:
1204-
matchRev, _ := body[BodyRev].(string)
1205-
generation, _ := ParseRevID(ctx, matchRev)
1206-
if generation < 0 {
1207-
return nil, base.HTTPErrorf(http.StatusBadRequest, "Invalid revision ID"), nil
1177+
// SyncFnDryrun Runs the given document body through a sync function and returns expiry, channels doc was placed in,
1178+
// access map for users, roles, handler errors and sync fn exceptions.
1179+
// If syncFn is provided, it will be used instead of the one configured on the database.
1180+
func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, newDoc, oldDoc *Document, userMeta, syncOptions map[string]any, syncFn string, errorLogFunc, infoLogFunc func(string)) (*channels.ChannelMapperOutput, error) {
1181+
mutableBody, metaMap, _, err := db.prepareSyncFn(oldDoc, newDoc)
1182+
if err != nil {
1183+
base.InfofCtx(ctx, base.KeyDiagnostic, "Failed to prepare to run sync function: %v", err)
1184+
return nil, err
12081185
}
1209-
generation++
12101186

1211-
// Create newDoc which will be used to pass around Body
1212-
newDoc := &Document{
1213-
ID: docID,
1187+
if userMeta != nil {
1188+
metaMap = userMeta
12141189
}
1215-
// Pull out attachments
1216-
newDoc.DocAttachments = GetBodyAttachments(body)
1217-
delete(body, BodyAttachments)
12181190

1219-
delete(body, BodyRevisions)
1220-
1221-
err := validateAPIDocUpdate(body)
1222-
if err != nil {
1223-
return nil, err, nil
1224-
}
1225-
bodyWithoutInternalProps, wasStripped := stripInternalProperties(body)
1226-
canonicalBytesForRevID, err := base.JSONMarshalCanonical(bodyWithoutInternalProps)
1227-
if err != nil {
1228-
return nil, err, nil
1191+
if syncOptions == nil {
1192+
syncOptions = MakeUserCtx(db.user, db.ScopeName, db.Name)
12291193
}
12301194

1231-
// We needed to keep _deleted around in the body until we generated a rev ID, but now we can ditch it.
1232-
_, isDeleted := body[BodyDeleted]
1233-
if isDeleted {
1234-
delete(body, BodyDeleted)
1195+
// fetch configured sync function if one is not provided
1196+
if syncFn == "" {
1197+
if db.ChannelMapper != nil {
1198+
syncFn = db.ChannelMapper.Function()
1199+
} else {
1200+
scopeAndCollectionName := db.ScopeAndCollectionName()
1201+
syncFn = channels.GetDefaultSyncFunction(scopeAndCollectionName.Scope, scopeAndCollectionName.Collection)
1202+
}
12351203
}
12361204

1237-
// and now we can finally update the newDoc body to be without any special properties
1238-
newDoc.UpdateBody(body)
1239-
1240-
// If no special properties were stripped and document wasn't deleted, the canonical bytes represent the current
1241-
// body. In this scenario, store canonical bytes as newDoc._rawBody
1242-
if !wasStripped && !isDeleted {
1243-
newDoc._rawBody = canonicalBytesForRevID
1205+
// create new sync runner instance for this dry run
1206+
jsTimeout := time.Duration(base.DefaultJavascriptTimeoutSecs) * time.Second
1207+
syncRunner, err := channels.NewSyncRunnerWithLogging(ctx, syncFn, jsTimeout, errorLogFunc, infoLogFunc)
1208+
if err != nil {
1209+
return nil, fmt.Errorf("failed to create sync runner: %v", err)
12441210
}
12451211

1246-
newRev := CreateRevIDWithBytes(generation, matchRev, canonicalBytesForRevID)
1247-
newDoc.RevID = newRev
1248-
mutableBody, metaMap, _, err := db.prepareSyncFn(oldDoc, newDoc)
1212+
jsOutput, err := syncRunner.Call(ctx, mutableBody, sgbucket.JSONString(oldDoc._rawBody), metaMap, syncOptions)
12491213
if err != nil {
1250-
base.InfofCtx(ctx, base.KeyDiagnostic, "Failed to prepare to run sync function: %v", err)
1251-
return nil, err, nil
1214+
return nil, &base.SyncFnDryRunError{Err: err}
12521215
}
12531216

1254-
output, err := db.ChannelMapper.MapToChannelsAndAccess(ctx, mutableBody, string(oldDoc._rawBody), metaMap,
1255-
MakeUserCtx(db.user, db.ScopeName, db.Name))
1256-
1257-
return output, nil, err
1217+
return jsOutput.(*channels.ChannelMapperOutput), nil
12581218
}
12591219

12601220
// resolveConflict runs the conflictResolverFunction with doc and newDoc. doc and newDoc's bodies and revision trees
@@ -1596,7 +1556,7 @@ func (db *DatabaseCollectionWithUser) storeOldBodyInRevTreeAndUpdateCurrent(ctx
15961556

15971557
func (db *DatabaseCollectionWithUser) prepareSyncFn(doc *Document, newDoc *Document) (mutableBody Body, metaMap map[string]interface{}, newRevID string, err error) {
15981558
// Marshal raw user xattrs for use in Sync Fn. If this fails we can bail out so we should do early as possible.
1599-
metaMap, err = doc.GetMetaMap(db.userXattrKey())
1559+
metaMap, err = doc.GetMetaMap(db.UserXattrKey())
16001560
if err != nil {
16011561
return
16021562
}

db/database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1679,7 +1679,7 @@ func (db *DatabaseCollectionWithUser) getResyncedDocument(ctx context.Context, d
16791679
base.WarnfCtx(ctx, "Error unmarshalling body %s/%s for sync function %s", base.UD(docid), rev.ID, err)
16801680
return
16811681
}
1682-
metaMap, err := doc.GetMetaMap(db.userXattrKey())
1682+
metaMap, err := doc.GetMetaMap(db.UserXattrKey())
16831683
if err != nil {
16841684
return
16851685
}

db/database_collection.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func (c *DatabaseCollection) unsupportedOptions() *UnsupportedOptions {
235235
// syncAndUserXattrKeys returns the xattr keys for the user and sync xattrs.
236236
func (c *DatabaseCollection) syncAndUserXattrKeys() []string {
237237
xattrKeys := []string{base.SyncXattrName}
238-
userXattrKey := c.userXattrKey()
238+
userXattrKey := c.UserXattrKey()
239239
if userXattrKey != "" {
240240
xattrKeys = append(xattrKeys, userXattrKey)
241241
}
@@ -248,15 +248,15 @@ func (c *DatabaseCollection) syncMouAndUserXattrKeys() []string {
248248
if c.useMou() {
249249
xattrKeys = append(xattrKeys, base.MouXattrName)
250250
}
251-
userXattrKey := c.userXattrKey()
251+
userXattrKey := c.UserXattrKey()
252252
if userXattrKey != "" {
253253
xattrKeys = append(xattrKeys, userXattrKey)
254254
}
255255
return xattrKeys
256256
}
257257

258258
// Returns the xattr key that will be accessible from the sync function. This is controlled at a database level.
259-
func (c *DatabaseCollection) userXattrKey() string {
259+
func (c *DatabaseCollection) UserXattrKey() string {
260260
return c.dbCtx.Options.UserXattrKey
261261
}
262262

db/document.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ func unmarshalDocument(docid string, data []byte) (*Document, error) {
391391

392392
// unmarshalDocumentWithXattrs populates individual xattrs on unmarshalDocumentWithXattrs from a provided xattrs map
393393
func (db *DatabaseCollection) unmarshalDocumentWithXattrs(ctx context.Context, docid string, data []byte, xattrs map[string][]byte, cas uint64, unmarshalLevel DocumentUnmarshalLevel) (doc *Document, err error) {
394-
return unmarshalDocumentWithXattrs(ctx, docid, data, xattrs[base.SyncXattrName], xattrs[base.MouXattrName], xattrs[db.userXattrKey()], cas, unmarshalLevel)
394+
return unmarshalDocumentWithXattrs(ctx, docid, data, xattrs[base.SyncXattrName], xattrs[base.MouXattrName], xattrs[db.UserXattrKey()], cas, unmarshalLevel)
395395

396396
}
397397

0 commit comments

Comments
 (0)