feat(syncing-server): add traffic abuse checks (#1014)

This commit is contained in:
Karol Sójko 2024-01-05 11:30:15 +01:00 committed by GitHub
parent 01641975c0
commit b7173346d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 377 additions and 47 deletions

View file

@ -28,7 +28,7 @@ const sendStatistics = async (
const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
const statistics = await metricsStore.getStatistics(
const statistics = await metricsStore.getMetricsSummary(
metricToProcess,
timestamp,
timestamp + Time.MicrosecondsInAMinute,
@ -64,7 +64,7 @@ const sendStatistics = async (
const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
const statistics = await metricsStore.getUserBasedStatistics(metricToProcess, timestamp)
const statistics = await metricsStore.getUserBasedMetricsSummary(metricToProcess, timestamp)
if (statistics.sampleCount === 0) {
continue

View file

@ -166,6 +166,7 @@ import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients
import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@ -472,6 +473,33 @@ export class ContainerConfigLoader {
})
// env vars
container
.bind(TYPES.Sync_STRICT_ABUSE_PROTECTION)
.toConstantValue(env.get('STRICT_ABUSE_PROTECTION', true) === 'true')
container
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD)
.toConstantValue(
env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) ? +env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) : 500,
)
container
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
.toConstantValue(
env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
? +env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
: 5,
)
container
.bind(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD)
.toConstantValue(
env.get('PAYLOAD_SIZE_ABUSE_THRESHOLD', true) ? +env.get('PAYLOAD_SIZE_ABUSE_THRESHOLD', true) : 20_000_000,
)
container
.bind(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
.toConstantValue(
env.get('PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
? +env.get('PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
: 5,
)
container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container
.bind(TYPES.Sync_REVISIONS_FREQUENCY)
@ -933,6 +961,14 @@ export class ContainerConfigLoader {
context.container.get(TYPES.Sync_SyncResponseFactory20200115),
)
})
container
.bind<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse)
.toConstantValue(
new CheckForTrafficAbuse(
container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
container.get<TimerInterface>(TYPES.Sync_Timer),
),
)
// Handlers
container
@ -1094,11 +1130,18 @@ export class ContainerConfigLoader {
.bind<BaseItemsController>(TYPES.Sync_BaseItemsController)
.toConstantValue(
new BaseItemsController(
container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
container.get<GetItem>(TYPES.Sync_GetItem),
container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<Logger>(TYPES.Sync_Logger),
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
),
)

View file

@ -42,6 +42,15 @@ const TYPES = {
Sync_VALET_TOKEN_SECRET: Symbol.for('Sync_VALET_TOKEN_SECRET'),
Sync_VALET_TOKEN_TTL: Symbol.for('Sync_VALET_TOKEN_TTL'),
Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for('Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING'),
Sync_STRICT_ABUSE_PROTECTION: Symbol.for('Sync_STRICT_ABUSE_PROTECTION'),
Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
'Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
),
Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD: Symbol.for('Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD'),
Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
'Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
),
// use cases
Sync_SyncItems: Symbol.for('Sync_SyncItems'),
Sync_CheckIntegrity: Symbol.for('Sync_CheckIntegrity'),
@ -85,6 +94,7 @@ const TYPES = {
Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
Sync_DumpItem: Symbol.for('Sync_DumpItem'),
Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
// Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(

View file

@ -1,25 +1,17 @@
import { Uuid } from '@standardnotes/domain-core'
import { Metric } from './Metric'
import { MetricsSummary } from './MetricsSummary'
export interface MetricsStoreInterface {
storeUserBasedMetric(metric: Metric, value: number, userUuid: string): Promise<void>
getUserBasedStatistics(
name: string,
timestamp: number,
): Promise<{
sum: number
max: number
min: number
sampleCount: number
}>
storeMetric(metric: Metric): Promise<void>
getStatistics(
name: string,
from: number,
to: number,
): Promise<{
sum: number
max: number
min: number
sampleCount: number
}>
storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void>
getUserBasedMetricsSummaryWithinTimeRange(dto: {
metricName: string
userUuid: Uuid
from: Date
to: Date
}): Promise<MetricsSummary>
getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary>
getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary>
}

View file

@ -0,0 +1,6 @@
export interface MetricsSummary {
sum: number
max: number
min: number
sampleCount: number
}

View file

@ -0,0 +1,97 @@
import { TimerInterface } from '@standardnotes/time'
import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
import { CheckForTrafficAbuse } from './CheckForTrafficAbuse'
import { MetricsSummary } from '../../../Metrics/MetricsSummary'
import { Metric } from '../../../Metrics/Metric'
describe('CheckForTrafficAbuse', () => {
let metricsStore: MetricsStoreInterface
let timer: TimerInterface
let timeframeLengthInMinutes: number
let threshold: number
const createUseCase = () => new CheckForTrafficAbuse(metricsStore, timer)
beforeEach(() => {
const metricsSummary: MetricsSummary = {
sum: 101,
max: 0,
min: 0,
sampleCount: 0,
}
metricsStore = {} as jest.Mocked<MetricsStoreInterface>
metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
timer = {} as TimerInterface
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(0)
timer.getUTCDateNMinutesAgo = jest.fn().mockReturnValue(new Date(0))
timer.getUTCDate = jest.fn().mockReturnValue(new Date(10))
timeframeLengthInMinutes = 5
threshold = 100
})
it('returns a failure result if the user uuid is invalid', async () => {
const result = await createUseCase().execute({
userUuid: 'invalid',
metricToCheck: Metric.NAMES.ItemOperation,
timeframeLengthInMinutes,
threshold,
})
expect(result.isFailed()).toBeTruthy()
})
it('return a failure result if the metric name is invalid', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
metricToCheck: 'invalid',
timeframeLengthInMinutes,
threshold,
})
expect(result.isFailed()).toBeTruthy()
})
it('returns a failure result if the metric summary is above the threshold', async () => {
const metricsSummary: MetricsSummary = {
sum: 101,
max: 0,
min: 0,
sampleCount: 0,
}
metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
metricToCheck: Metric.NAMES.ItemOperation,
timeframeLengthInMinutes,
threshold,
})
expect(result.isFailed()).toBeTruthy()
})
it('returns a success result if the metric summary is below the threshold', async () => {
const metricsSummary: MetricsSummary = {
sum: 99,
max: 0,
min: 0,
sampleCount: 0,
}
metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
metricToCheck: Metric.NAMES.ItemOperation,
timeframeLengthInMinutes,
threshold,
})
expect(result.isFailed()).toBeFalsy()
})
})

View file

@ -0,0 +1,44 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { CheckForTrafficAbuseDTO } from './CheckForTrafficAbuseDTO'
import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
import { Metric } from '../../../Metrics/Metric'
import { MetricsSummary } from '../../../Metrics/MetricsSummary'
export class CheckForTrafficAbuse implements UseCaseInterface<MetricsSummary> {
constructor(
private metricsStore: MetricsStoreInterface,
private timer: TimerInterface,
) {}
async execute(dto: CheckForTrafficAbuseDTO): Promise<Result<MetricsSummary>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const metricToCheckOrError = Metric.create({
name: dto.metricToCheck,
timestamp: this.timer.getTimestampInMicroseconds(),
})
if (metricToCheckOrError.isFailed()) {
return Result.fail(metricToCheckOrError.getError())
}
const metricToCheck = metricToCheckOrError.getValue()
const metricsSummary = await this.metricsStore.getUserBasedMetricsSummaryWithinTimeRange({
metricName: metricToCheck.props.name,
userUuid,
from: this.timer.getUTCDateNMinutesAgo(dto.timeframeLengthInMinutes),
to: this.timer.getUTCDate(),
})
if (metricsSummary.sum > dto.threshold) {
return Result.fail(`Traffic abuse detected for metric: ${metricToCheck.props.name}`)
}
return Result.ok(metricsSummary)
}
}

View file

@ -0,0 +1,6 @@
export interface CheckForTrafficAbuseDTO {
userUuid: string
metricToCheck: string
timeframeLengthInMinutes: number
threshold: number
}

View file

@ -145,7 +145,7 @@ export class SaveNewItem implements UseCaseInterface<Item> {
timestamp: this.timer.getTimestampInMicroseconds(),
}).getValue(),
newItem.props.contentSize,
userUuid.value,
userUuid,
)
await this.metricsStore.storeMetric(

View file

@ -186,7 +186,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
timestamp: this.timer.getTimestampInMicroseconds(),
}).getValue(),
dto.existingItem.props.contentSize,
userUuid.value,
userUuid,
)
/* istanbul ignore next */

View file

@ -1,11 +1,16 @@
import { Uuid } from '@standardnotes/domain-core'
import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
import { Metric } from '../../Domain/Metrics/Metric'
import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
export class DummyMetricStore implements MetricsStoreInterface {
async getUserBasedStatistics(
_name: string,
_timestamp: number,
): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
async getUserBasedMetricsSummaryWithinTimeRange(_dto: {
metricName: string
userUuid: Uuid
from: Date
to: Date
}): Promise<MetricsSummary> {
return {
sum: 0,
max: 0,
@ -14,7 +19,16 @@ export class DummyMetricStore implements MetricsStoreInterface {
}
}
async storeUserBasedMetric(_metric: Metric, _value: number, _userUuid: string): Promise<void> {
async getUserBasedMetricsSummary(_name: string, _timestamp: number): Promise<MetricsSummary> {
return {
sum: 0,
max: 0,
min: 0,
sampleCount: 0,
}
}
async storeUserBasedMetric(_metric: Metric, _value: number, _userUuid: Uuid): Promise<void> {
// do nothing
}
@ -22,11 +36,7 @@ export class DummyMetricStore implements MetricsStoreInterface {
// do nothing
}
async getStatistics(
_name: string,
_from: number,
_to: number,
): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
async getMetricsSummary(_name: string, _from: number, _to: number): Promise<MetricsSummary> {
return {
sum: 0,
max: 0,

View file

@ -11,18 +11,42 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { BaseItemsController } from './Base/BaseItemsController'
import { MapperInterface } from '@standardnotes/domain-core'
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
import { CheckForTrafficAbuse } from '../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
import { Logger } from 'winston'
@controller('/items', TYPES.Sync_AuthMiddleware)
export class AnnotatedItemsController extends BaseItemsController {
constructor(
@inject(TYPES.Sync_CheckForTrafficAbuse) override checkForTrafficAbuse: CheckForTrafficAbuse,
@inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
@inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
@inject(TYPES.Sync_GetItem) override getItem: GetItem,
@inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
@inject(TYPES.Sync_SyncResponseFactoryResolver)
override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
@inject(TYPES.Sync_Logger) override logger: Logger,
@inject(TYPES.Sync_STRICT_ABUSE_PROTECTION) override strictAbuseProtection: boolean,
@inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
override itemOperationsAbuseTimeframeLengthInMinutes: number,
@inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD) override itemOperationsAbuseThreshold: number,
@inject(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD) override payloadSizeAbuseThreshold: number,
@inject(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
override payloadSizeAbuseTimeframeLengthInMinutes: number,
) {
super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
super(
checkForTrafficAbuse,
syncItems,
checkIntegrity,
getItem,
itemHttpMapper,
syncResponseFactoryResolver,
logger,
strictAbuseProtection,
itemOperationsAbuseTimeframeLengthInMinutes,
itemOperationsAbuseThreshold,
payloadSizeAbuseThreshold,
payloadSizeAbuseTimeframeLengthInMinutes,
)
}
@httpPost('/sync')

View file

@ -11,14 +11,24 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
import { ItemHash } from '../../../Domain/Item/ItemHash'
import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
import { Metric } from '../../../Domain/Metrics/Metric'
import { Logger } from 'winston'
export class BaseItemsController extends BaseHttpController {
constructor(
protected checkForTrafficAbuse: CheckForTrafficAbuse,
protected syncItems: SyncItems,
protected checkIntegrity: CheckIntegrity,
protected getItem: GetItem,
protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
protected logger: Logger,
protected strictAbuseProtection: boolean,
protected itemOperationsAbuseTimeframeLengthInMinutes: number,
protected itemOperationsAbuseThreshold: number,
protected payloadSizeAbuseThreshold: number,
protected payloadSizeAbuseTimeframeLengthInMinutes: number,
private controllerContainer?: ControllerContainerInterface,
) {
super()
@ -31,6 +41,37 @@ export class BaseItemsController extends BaseHttpController {
}
async sync(request: Request, response: Response): Promise<results.JsonResult> {
const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ItemOperation,
userUuid: response.locals.user.uuid,
threshold: this.itemOperationsAbuseThreshold,
timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
})
if (checkForItemOperationsAbuseResult.isFailed()) {
this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
userId: response.locals.user.uuid,
})
if (this.strictAbuseProtection) {
return this.json({ error: { message: checkForItemOperationsAbuseResult.getError() } }, 429)
}
}
const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ContentSizeUtilized,
userUuid: response.locals.user.uuid,
threshold: this.payloadSizeAbuseThreshold,
timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
})
if (checkForPayloadSizeAbuseResult.isFailed()) {
this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
userId: response.locals.user.uuid,
})
if (this.strictAbuseProtection) {
return this.json({ error: { message: checkForPayloadSizeAbuseResult.getError() } }, 429)
}
}
const itemHashes: ItemHash[] = []
if ('items' in request.body) {
for (const itemHashInput of request.body.items) {

View file

@ -3,6 +3,8 @@ import { TimerInterface } from '@standardnotes/time'
import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
import { Metric } from '../../Domain/Metrics/Metric'
import { Uuid } from '@standardnotes/domain-core'
import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
export class RedisMetricStore implements MetricsStoreInterface {
private readonly METRIC_PREFIX = 'metric'
@ -13,15 +15,61 @@ export class RedisMetricStore implements MetricsStoreInterface {
private timer: TimerInterface,
) {}
async storeUserBasedMetric(metric: Metric, value: number, userUuid: string): Promise<void> {
async getUserBasedMetricsSummaryWithinTimeRange(dto: {
metricName: string
userUuid: Uuid
from: Date
to: Date
}): Promise<MetricsSummary> {
const keys = this.getKeysRepresentingMinutesBetweenFromAndTo(dto.from, dto.to)
let sum = 0
let max = 0
let min = 0
let sampleCount = 0
const values = await this.redisClient.mget(
keys.map((key) => `${this.METRIC_PER_USER_PREFIX}:${dto.userUuid.value}:${dto.metricName}:${key}`),
)
for (const value of values) {
if (!value) {
continue
}
const valueAsNumber = Number(value)
sum += valueAsNumber
sampleCount++
if (valueAsNumber > max) {
max = valueAsNumber
}
if (valueAsNumber < min) {
min = valueAsNumber
}
}
return {
sum,
max,
min,
sampleCount,
}
}
async storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void> {
const date = this.timer.convertMicrosecondsToDate(metric.props.timestamp)
const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
const key = `${this.METRIC_PER_USER_PREFIX}:${userUuid}:${metric.props.name}:${dateToTheMinuteString}`
const key = `${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${metric.props.name}:${dateToTheMinuteString}`
const pipeline = this.redisClient.pipeline()
pipeline.incrbyfloat(key, value)
pipeline.incr(`${this.METRIC_PER_USER_PREFIX}:${userUuid}:${Metric.NAMES.ItemOperation}:${dateToTheMinuteString}`)
pipeline.incr(
`${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${Metric.NAMES.ItemOperation}:${dateToTheMinuteString}`,
)
const expirationTime = 60 * 60 * 24
pipeline.expire(key, expirationTime)
@ -29,10 +77,7 @@ export class RedisMetricStore implements MetricsStoreInterface {
await pipeline.exec()
}
async getUserBasedStatistics(
name: string,
timestamp: number,
): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
async getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary> {
const date = this.timer.convertMicrosecondsToDate(timestamp)
const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
@ -74,11 +119,7 @@ export class RedisMetricStore implements MetricsStoreInterface {
}
}
async getStatistics(
name: string,
from: number,
to: number,
): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
async getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary> {
const keysRepresentingSecondsBetweenFromAndTo = this.getKeysRepresentingSecondsBetweenFromAndTo(from, to)
let sum = 0
@ -132,6 +173,22 @@ export class RedisMetricStore implements MetricsStoreInterface {
await pipeline.exec()
}
private getKeysRepresentingMinutesBetweenFromAndTo(from: Date, to: Date): string[] {
const keys: string[] = []
let currentMinute = from
while (currentMinute <= to) {
const dateToTheMinuteString = this.timer.convertDateToFormattedString(currentMinute, 'YYYY-MM-DD HH:mm')
keys.push(dateToTheMinuteString)
currentMinute = new Date(currentMinute.getTime() + 60 * 1000)
}
return keys
}
private getKeysRepresentingSecondsBetweenFromAndTo(from: number, to: number): string[] {
const keys: string[] = []