Skip to content

Commit 8460b55

Browse files
authored
Add logging and metrics to rate limit logic (#622)
Add logging and metrics to rate limit logic - Add log to user method limit - Add log to budget limit - Add metric to method limit - Capture methodName in user method limit Signed-off-by: Nana Essilfie-Conduah <[email protected]>
1 parent 91a813d commit 8460b55

File tree

7 files changed

+54
-21
lines changed

7 files changed

+54
-21
lines changed

packages/relay/src/lib/clients/sdkClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class SDKClient {
133133

134134
const duration = parseInt(process.env.HBAR_RATE_LIMIT_DURATION!);
135135
const total = parseInt(process.env.HBAR_RATE_LIMIT_TINYBAR!);
136-
this.hbarLimiter = new HbarLimit(Date.now(), total, duration);
136+
this.hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration);
137137
}
138138

139139
async getAccountBalance(account: string, callerName: string, requestId?: string): Promise<AccountBalance> {

packages/relay/src/lib/hbarlimiter/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
*
1919
*/
2020

21+
import { Logger } from 'pino';
22+
2123
export default class HbarLimit {
2224
private enabled: boolean = false;
2325
private remainingBudget: number;
2426
private duration: number = 0;
2527
private total: number = 0;
2628
private reset: number;
29+
private logger: Logger;
2730

28-
constructor(currentDateNow: number, total: number, duration: number) {
31+
constructor(logger: Logger, currentDateNow: number, total: number, duration: number) {
32+
this.logger = logger;
2933
this.enabled = false;
3034

3135
if (total && duration) {
@@ -48,7 +52,13 @@ export default class HbarLimit {
4852
if (this.shouldResetLimiter(currentDateNow)){
4953
this.resetLimiter(currentDateNow);
5054
}
51-
return this.remainingBudget <= 0 ? true : false;
55+
56+
if (this.remainingBudget <= 0) {
57+
this.logger.warn(`Rate limit incoming calls, ${this.remainingBudget} out of ${this.total} tℏ left in relay budget until ${this.reset}`);
58+
return true;
59+
}
60+
61+
return false;
5262
}
5363

5464
/**

packages/relay/tests/lib/hbarLimiter.spec.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
*/
2020

2121
import { expect } from 'chai';
22+
import pino from 'pino';
2223
import HbarLimit from '../../src/lib/hbarlimiter';
2324

25+
const logger = pino();
26+
2427
describe('HBAR Rate Limiter', async function () {
2528
this.timeout(20000);
2629
let rateLimiter: HbarLimit;
@@ -35,7 +38,7 @@ describe('HBAR Rate Limiter', async function () {
3538
});
3639

3740
it('should be disabled, if we pass invalid total', async function () {
38-
rateLimiter = new HbarLimit(currentDateNow, invalidTotal, validDuration);
41+
rateLimiter = new HbarLimit(logger, currentDateNow, invalidTotal, validDuration);
3942

4043
const isEnabled = rateLimiter.isEnabled();
4144
const limiterResetTime = rateLimiter.getResetTime();
@@ -50,7 +53,7 @@ describe('HBAR Rate Limiter', async function () {
5053
});
5154

5255
it('should be disabled, if we pass invalid duration', async function () {
53-
rateLimiter = new HbarLimit(currentDateNow, validTotal, invalidDuration);
56+
rateLimiter = new HbarLimit(logger, currentDateNow, validTotal, invalidDuration);
5457

5558
const isEnabled = rateLimiter.isEnabled();
5659
const limiterResetTime = rateLimiter.getResetTime();
@@ -65,7 +68,7 @@ describe('HBAR Rate Limiter', async function () {
6568
});
6669

6770
it('should be disabled, if we pass both invalid duration and total', async function () {
68-
rateLimiter = new HbarLimit(currentDateNow, invalidTotal, invalidDuration);
71+
rateLimiter = new HbarLimit(logger, currentDateNow, invalidTotal, invalidDuration);
6972

7073
const isEnabled = rateLimiter.isEnabled();
7174
const limiterResetTime = rateLimiter.getResetTime();
@@ -80,7 +83,7 @@ describe('HBAR Rate Limiter', async function () {
8083
});
8184

8285
it('should be enabled, if we pass valid duration and total', async function () {
83-
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
86+
rateLimiter = new HbarLimit(logger, currentDateNow, validTotal, validDuration);
8487

8588
const isEnabled = rateLimiter.isEnabled();
8689
const limiterResetTime = rateLimiter.getResetTime();
@@ -95,7 +98,7 @@ describe('HBAR Rate Limiter', async function () {
9598

9699
it('should not rate limit', async function () {
97100
const cost = 10000000;
98-
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
101+
rateLimiter = new HbarLimit(logger, currentDateNow, validTotal, validDuration);
99102
rateLimiter.addExpense(cost, currentDateNow);
100103

101104
const isEnabled = rateLimiter.isEnabled();
@@ -111,7 +114,7 @@ describe('HBAR Rate Limiter', async function () {
111114

112115
it('should rate limit', async function () {
113116
const cost = 1000000000;
114-
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
117+
rateLimiter = new HbarLimit(logger, currentDateNow, validTotal, validDuration);
115118
rateLimiter.addExpense(cost, currentDateNow);
116119

117120
const isEnabled = rateLimiter.isEnabled();
@@ -127,7 +130,7 @@ describe('HBAR Rate Limiter', async function () {
127130

128131
it('should reset budget, while checking if we should rate limit', async function () {
129132
const cost = 1000000000;
130-
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
133+
rateLimiter = new HbarLimit(logger, currentDateNow, validTotal, validDuration);
131134
rateLimiter.addExpense(cost, currentDateNow);
132135

133136
const isEnabled = rateLimiter.isEnabled();
@@ -144,7 +147,7 @@ describe('HBAR Rate Limiter', async function () {
144147

145148
it('should reset budget, while adding expense', async function () {
146149
const cost = 1000000000;
147-
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
150+
rateLimiter = new HbarLimit(logger, currentDateNow, validTotal, validDuration);
148151

149152
rateLimiter.addExpense(cost, currentDateNow);
150153
const shouldRateLimitBefore = rateLimiter.shouldLimit(currentDateNow);

packages/server/src/koaJsonRpc/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import RateLimit from '../ratelimit';
2525
import parse from 'co-body';
2626
import dotenv from 'dotenv';
2727
import path from 'path';
28+
import { Logger } from 'pino';
2829
import {
2930
ParseError,
3031
InvalidRequest,
@@ -34,6 +35,7 @@ import {
3435
Unauthorized
3536
} from './lib/RpcError';
3637
import Koa from 'koa';
38+
import { Registry } from 'prom-client';
3739

3840
const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
3941
dotenv.config({ path: path.resolve(__dirname, '../../../../../.env') });
@@ -48,7 +50,7 @@ export default class KoaJsonRpc {
4850
private ratelimit: RateLimit;
4951
private koaApp: Koa<Koa.DefaultState, Koa.DefaultContext>;
5052

51-
constructor(opts?) {
53+
constructor(logger: Logger, register: Registry, opts?) {
5254
this.koaApp = new Koa();
5355
this.limit = '1mb';
5456
this.duration = parseInt(process.env.LIMIT_DURATION!);
@@ -59,7 +61,7 @@ export default class KoaJsonRpc {
5961
this.limit = opts.limit || this.limit;
6062
this.duration = opts.limit || this.limit;
6163
}
62-
this.ratelimit = new RateLimit(this.duration);
64+
this.ratelimit = new RateLimit(logger.child({ name: 'ip-rate-limit' }), register, this.duration);
6365
}
6466

6567
useRpc(name, func) {
@@ -109,7 +111,7 @@ export default class KoaJsonRpc {
109111
const methodName = body.method;
110112
const methodTotalLimit = this.registryTotal[methodName];
111113
if (this.ratelimit.shouldRateLimit(ctx.ip, methodName, methodTotalLimit)) {
112-
ctx.body = jsonResp(body.id, new IPRateLimitExceeded(), undefined);
114+
ctx.body = jsonResp(body.id, new IPRateLimitExceeded(methodName), undefined);
113115
return;
114116
}
115117

packages/server/src/koaJsonRpc/lib/RpcError.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export class ServerError extends JsonRpcError {
8585
}
8686

8787
export class IPRateLimitExceeded extends JsonRpcError {
88-
constructor() {
89-
super('IP Rate limit exceeded', -32605, undefined);
88+
constructor(methodName) {
89+
super(`IP Rate limit exceeded on ${methodName}`, -32605, undefined);
9090
}
9191
}
9292

packages/server/src/ratelimit/index.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,28 @@
1818
*
1919
*/
2020

21+
import { Logger } from 'pino';
22+
import { Gauge, Registry } from 'prom-client';
23+
2124
export default class RateLimit {
22-
duration: number;
23-
database: any;
25+
private duration: number;
26+
private database: any;
27+
private logger: Logger;
28+
private ipRateLimitGauge: Gauge;
2429

25-
constructor(duration) {
30+
constructor(logger: Logger, register: Registry, duration) {
31+
this.logger = logger;
2632
this.duration = duration;
2733
this.database = Object.create(null);
34+
35+
const metricGaugeName = 'rpc_relay_ip_rate_limit';
36+
register.removeSingleMetric(metricGaugeName);
37+
this.ipRateLimitGauge = new Gauge({
38+
name: metricGaugeName,
39+
help: 'Relay ip rate limit gauge',
40+
labelNames: ['methodName'],
41+
registers: [register],
42+
});
2843
}
2944

3045
shouldRateLimit(ip: string, methodName: string, total: number): boolean {
@@ -34,6 +49,9 @@ export default class RateLimit {
3449
this.decreaseRemaining(ip, methodName);
3550
return false;
3651
}
52+
53+
this.ipRateLimitGauge.labels(methodName).inc(1);
54+
this.logger.warn(`Rate limit call to ${methodName}, ${this.database[ip].methodInfo[methodName].remaining} out of ${total} calls remaining`);
3755
return true;
3856
} else {
3957
this.reset(ip, methodName, total);

packages/server/src/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const cors = require('koa-cors');
4242
const logger = mainLogger.child({ name: 'rpc-server' });
4343
const register = new Registry();
4444
const relay: Relay = new RelayImpl(logger, register);
45-
const app = new KoaJsonRpc();
45+
const app = new KoaJsonRpc(logger, register);
4646

4747
const REQUEST_ID_STRING = `Request ID: `;
4848
const responseSuccessStatusCode = '200';
@@ -144,7 +144,7 @@ const logAndHandleResponse = async (methodName, methodFunction) => {
144144
methodResponseHistogram.labels(methodName, status).observe(ms);
145145
logger.info(`${messagePrefix} ${status} ${ms} ms `);
146146
if (response instanceof JsonRpcError) {
147-
logger.error(`returning error to sender: ${requestIdPrefix} ${response.message}`)
147+
logger.error(`returning error to sender: ${requestIdPrefix} ${response.message}`);
148148
return new JsonRpcError({
149149
name: response.name,
150150
code: response.code,

0 commit comments

Comments
 (0)