mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-02 18:25:42 +00:00
Merge pull request #17675 from overleaf/bg-session-mitigation-redis-store-metrics
add CustomSessionStore class to handle session metrics and logging GitOrigin-RevId: 49d4cda9fd94a8801adb33e894be239dc38ad544
This commit is contained in:
parent
b84e76b201
commit
cdf31ed91c
2 changed files with 86 additions and 36 deletions
services/web/app/src/infrastructure
84
services/web/app/src/infrastructure/CustomSessionStore.js
Normal file
84
services/web/app/src/infrastructure/CustomSessionStore.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
const session = require('express-session')
|
||||
const RedisStore = require('connect-redis')(session)
|
||||
const metrics = require('@overleaf/metrics')
|
||||
const logger = require('@overleaf/logger')
|
||||
const SessionManager = require('../Features/Authentication/SessionManager')
|
||||
|
||||
const MAX_SESSION_SIZE_THRESHOLD = 4096
|
||||
|
||||
// Define a custom session store to record session metrics and log large
|
||||
// anonymous sessions for debugging purposes
|
||||
class CustomSessionStore extends RedisStore {
|
||||
static largestSessionSize = 3 * 1024 // ignore sessions smaller than 3KB
|
||||
|
||||
static metric(method, sess) {
|
||||
let type // type of session: 'logged-in', 'anonymous', or 'na' (not available)
|
||||
if (sess) {
|
||||
type = SessionManager.isUserLoggedIn(sess) ? 'logged-in' : 'anonymous'
|
||||
} else {
|
||||
type = 'na'
|
||||
}
|
||||
const size = sess ? JSON.stringify(sess).length : 0
|
||||
// record the number of redis session operations
|
||||
metrics.inc('session.store.count', 1, {
|
||||
method,
|
||||
type,
|
||||
status: size > MAX_SESSION_SIZE_THRESHOLD ? 'oversize' : 'normal',
|
||||
})
|
||||
// record the redis session bandwidth for get/set operations
|
||||
if (method === 'get' || method === 'set') {
|
||||
metrics.count('session.store.bytes', size, { method, type })
|
||||
}
|
||||
// log the largest anonymous session seen so far
|
||||
if (type === 'anonymous' && size > CustomSessionStore.largestSessionSize) {
|
||||
CustomSessionStore.largestSessionSize = size
|
||||
logger.warn(
|
||||
{ redactedSession: redactSession(sess), largestSessionSize: size },
|
||||
'largest session size seen'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Override the get, set, touch, and destroy methods to record metrics
|
||||
get(sid, cb) {
|
||||
super.get(sid, (err, ...args) => {
|
||||
if (args[0]) {
|
||||
CustomSessionStore.metric('get', args[0])
|
||||
}
|
||||
cb(err, ...args)
|
||||
})
|
||||
}
|
||||
|
||||
set(sid, sess, cb) {
|
||||
CustomSessionStore.metric('set', sess)
|
||||
super.set(sid, sess, cb)
|
||||
}
|
||||
|
||||
touch(sid, sess, cb) {
|
||||
CustomSessionStore.metric('touch', sess)
|
||||
super.touch(sid, sess, cb)
|
||||
}
|
||||
|
||||
destroy(sid, cb) {
|
||||
// for the destroy method we don't have access to the session object itself
|
||||
CustomSessionStore.metric('destroy')
|
||||
super.destroy(sid, cb)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to return a redacted version of session object
|
||||
// so we can identify the largest keys without exposing sensitive
|
||||
// data
|
||||
function redactSession(sess) {
|
||||
// replace all string values with '***' of the same length
|
||||
return JSON.parse(
|
||||
JSON.stringify(sess, (key, value) => {
|
||||
if (typeof value === 'string') {
|
||||
return '*'.repeat(value.length)
|
||||
}
|
||||
return value
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = CustomSessionStore
|
|
@ -16,7 +16,7 @@ const SessionAutostartMiddleware = require('./SessionAutostartMiddleware')
|
|||
const SessionStoreManager = require('./SessionStoreManager')
|
||||
const AnalyticsManager = require('../Features/Analytics/AnalyticsManager')
|
||||
const session = require('express-session')
|
||||
const RedisStore = require('connect-redis')(session)
|
||||
const CustomSessionStore = require('./CustomSessionStore')
|
||||
const bodyParser = require('./BodyParserWrapper')
|
||||
const methodOverride = require('method-override')
|
||||
const cookieParser = require('cookie-parser')
|
||||
|
@ -48,42 +48,8 @@ const STATIC_CACHE_AGE = Settings.cacheStaticAssets
|
|||
? oneDayInMilliseconds * 365
|
||||
: 0
|
||||
|
||||
// Define a custom session store to record the largest session sizes
|
||||
// seen for anonymous users
|
||||
class CustomSessionStore extends RedisStore {
|
||||
static largestSessionSize = 2048 // ignore sessions smaller than 2KB
|
||||
|
||||
static trackAnonymousSessionSize(sess) {
|
||||
const isLoggedIn = SessionManager.isUserLoggedIn(sess)
|
||||
if (!isLoggedIn) {
|
||||
const len = JSON.stringify(sess, (key, value) => {
|
||||
if (key === 'hashedPassword' && value?.length > 0) {
|
||||
return '*'.repeat(value.length)
|
||||
}
|
||||
return value
|
||||
}).length
|
||||
if (len > CustomSessionStore.largestSessionSize) {
|
||||
CustomSessionStore.largestSessionSize = len
|
||||
logger.warn({ sess, sessionSize: len }, 'largest session size seen')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(sid, sess, cb) {
|
||||
CustomSessionStore.trackAnonymousSessionSize(sess)
|
||||
super.set(sid, sess, cb)
|
||||
}
|
||||
|
||||
touch(sid, sess, cb) {
|
||||
CustomSessionStore.trackAnonymousSessionSize(sess)
|
||||
super.touch(sid, sess, cb)
|
||||
}
|
||||
}
|
||||
|
||||
// Init the session store
|
||||
const sessionStore = new CustomSessionStore(
|
||||
new RedisStore({ client: sessionsRedisClient })
|
||||
)
|
||||
const sessionStore = new CustomSessionStore({ client: sessionsRedisClient })
|
||||
|
||||
const app = express()
|
||||
|
||||
|
|
Loading…
Reference in a new issue