Skip to content

Commit f4a8679

Browse files
committed
feat: prometheus instrumentation
1 parent e9959eb commit f4a8679

File tree

9 files changed

+118
-29
lines changed

9 files changed

+118
-29
lines changed

packages/services/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"devDependencies": {
1414
"@aws-sdk/client-s3": "3.723.0",
1515
"@aws-sdk/s3-request-presigner": "3.723.0",
16+
"@bentocache/plugin-prometheus": "0.2.0",
1617
"@date-fns/utc": "2.1.0",
1718
"@graphql-hive/core": "workspace:*",
1819
"@graphql-inspector/core": "5.1.0-alpha-20231208113249-34700c8a",

packages/services/api/src/create.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { IdTranslator } from './modules/shared/providers/id-translator';
5454
import { Logger } from './modules/shared/providers/logger';
5555
import { Mutex } from './modules/shared/providers/mutex';
5656
import { PG_POOL_CONFIG } from './modules/shared/providers/pg-pool';
57+
import { PrometheusConfig } from './modules/shared/providers/prometheus-config';
5758
import { HivePubSub, PUB_SUB_CONFIG } from './modules/shared/providers/pub-sub';
5859
import { REDIS_INSTANCE } from './modules/shared/providers/redis';
5960
import { S3_CONFIG, type S3Config } from './modules/shared/providers/s3-config';
@@ -119,6 +120,7 @@ export function createRegistry({
119120
organizationOIDC,
120121
pubSub,
121122
appDeploymentsEnabled,
123+
prometheus,
122124
}: {
123125
logger: Logger;
124126
storage: Storage;
@@ -164,6 +166,7 @@ export function createRegistry({
164166
organizationOIDC: boolean;
165167
pubSub: HivePubSub;
166168
appDeploymentsEnabled: boolean;
169+
prometheus: null | Record<string, unknown>;
167170
}) {
168171
const s3Config: S3Config = [
169172
{
@@ -323,6 +326,12 @@ export function createRegistry({
323326
scope: Scope.Operation,
324327
deps: [CONTEXT],
325328
},
329+
{
330+
provide: PrometheusConfig,
331+
useFactory() {
332+
return new PrometheusConfig(!!prometheus);
333+
},
334+
},
326335
];
327336

328337
if (emailsEndpoint) {

packages/services/api/src/modules/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createModule } from 'graphql-modules';
22
import { AuditLogManager } from '../audit-logs/providers/audit-logs-manager';
33
import { AuthManager } from './providers/auth-manager';
44
import { OrganizationAccess } from './providers/organization-access';
5+
import { OrganizationAccessTokenValidationCache } from './providers/organization-access-token-validation-cache';
56
import { ProjectAccess } from './providers/project-access';
67
import { TargetAccess } from './providers/target-access';
78
import { UserManager } from './providers/user-manager';
@@ -20,5 +21,6 @@ export const authModule = createModule({
2021
ProjectAccess,
2122
TargetAccess,
2223
AuditLogManager,
24+
OrganizationAccessTokenValidationCache,
2325
],
2426
});

packages/services/api/src/modules/auth/lib/organization-access-token-strategy.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
11
import * as crypto from 'node:crypto';
2-
import { BentoCache, bentostore } from 'bentocache';
3-
import { memoryDriver } from 'bentocache/build/src/drivers/memory';
42
import { type FastifyReply, type FastifyRequest } from '@hive/service-common';
53
import * as OrganizationAccessKey from '../../organization/lib/organization-access-key';
64
import { OrganizationAccessTokensCache } from '../../organization/providers/organization-access-tokens-cache';
75
import { Logger } from '../../shared/providers/logger';
6+
import { OrganizationAccessTokenValidationCache } from '../providers/organization-access-token-validation-cache';
87
import { AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz';
98

10-
const cache = new BentoCache({
11-
default: 'organizationAccessTokenValidation',
12-
stores: {
13-
organizationAccessTokenValidation: bentostore().useL1Layer(
14-
memoryDriver({
15-
maxItems: 10_000,
16-
}),
17-
),
18-
},
19-
});
20-
219
function hashToken(token: string) {
2210
return crypto.createHash('sha256').update(token).digest('hex');
2311
}
@@ -50,12 +38,18 @@ export class OrganizationAccessTokenSession extends Session {
5038
export class OrganizationAccessTokenStrategy extends AuthNStrategy<OrganizationAccessTokenSession> {
5139
private logger: Logger;
5240

53-
private cache: OrganizationAccessTokensCache;
41+
private organizationAccessTokenCache: OrganizationAccessTokensCache;
42+
private organizationAccessTokenValidationCache: OrganizationAccessTokenValidationCache;
5443

55-
constructor(deps: { logger: Logger; cache: OrganizationAccessTokensCache }) {
44+
constructor(deps: {
45+
logger: Logger;
46+
organizationAccessTokensCache: OrganizationAccessTokensCache;
47+
organizationAccessTokenValidationCache: OrganizationAccessTokenValidationCache;
48+
}) {
5649
super();
5750
this.logger = deps.logger.child({ module: 'OrganizationAccessTokenStrategy' });
58-
this.cache = deps.cache;
51+
this.organizationAccessTokenCache = deps.organizationAccessTokensCache;
52+
this.organizationAccessTokenValidationCache = deps.organizationAccessTokenValidationCache;
5953
}
6054

6155
async parse(args: {
@@ -89,14 +83,16 @@ export class OrganizationAccessTokenStrategy extends AuthNStrategy<OrganizationA
8983
return null;
9084
}
9185

92-
const organizationAccessToken = await this.cache.get(result.accessKey.id);
86+
const organizationAccessToken = await this.organizationAccessTokenCache.get(
87+
result.accessKey.id,
88+
);
9389
if (!organizationAccessToken) {
9490
return null;
9591
}
9692

9793
// let's hash it so we do not store the plain private key in memory
9894
const key = hashToken(accessToken);
99-
const isHashMatch = await cache.getOrSetForever({
95+
const isHashMatch = await this.organizationAccessTokenValidationCache.getOrSetForever({
10096
factory: () =>
10197
OrganizationAccessKey.verify(result.accessKey.privateKey, organizationAccessToken.hash),
10298
key,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { BentoCache, bentostore } from 'bentocache';
2+
import { memoryDriver } from 'bentocache/build/src/drivers/memory';
3+
import { Injectable, Scope } from 'graphql-modules';
4+
import { prometheusPlugin } from '@bentocache/plugin-prometheus';
5+
import { PrometheusConfig } from '../../shared/providers/prometheus-config';
6+
7+
/**
8+
* Cache for performant OrganizationAccessToken lookups.
9+
*/
10+
@Injectable({
11+
scope: Scope.Singleton,
12+
global: true,
13+
})
14+
export class OrganizationAccessTokenValidationCache {
15+
private cache: BentoCache<{ store: ReturnType<typeof bentostore> }>;
16+
17+
constructor(prometheusConfig: PrometheusConfig) {
18+
this.cache = new BentoCache({
19+
default: 'organizationAccessTokenValidation',
20+
plugins: prometheusConfig.isEnabled
21+
? [
22+
prometheusPlugin({
23+
prefix: 'bentocache_organization_access_token_validation',
24+
}),
25+
]
26+
: undefined,
27+
stores: {
28+
organizationAccessTokenValidation: bentostore().useL1Layer(
29+
memoryDriver({
30+
maxItems: 10_000,
31+
prefix: 'bentocache:organization-access-token-validation',
32+
}),
33+
),
34+
},
35+
});
36+
}
37+
38+
getOrSetForever: typeof this.cache.getOrSetForever = (...args) =>
39+
this.cache.getOrSetForever(...args);
40+
}

packages/services/api/src/modules/organization/providers/organization-access-tokens-cache.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { redisDriver } from 'bentocache/build/src/drivers/redis';
44
import { Inject, Injectable, Scope } from 'graphql-modules';
55
import Redis from 'ioredis';
66
import type { DatabasePool } from 'slonik';
7+
import { prometheusPlugin } from '@bentocache/plugin-prometheus';
78
import { Logger } from '../../shared/providers/logger';
89
import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool';
10+
import { PrometheusConfig } from '../../shared/providers/prometheus-config';
911
import { REDIS_INSTANCE } from '../../shared/providers/redis';
1012
import { findById, type OrganizationAccessToken } from './organization-access-tokens';
1113

@@ -24,10 +26,18 @@ export class OrganizationAccessTokensCache {
2426
@Inject(REDIS_INSTANCE) redis: Redis,
2527
@Inject(PG_POOL_CONFIG) pool: DatabasePool,
2628
logger: Logger,
29+
prometheusConfig: PrometheusConfig,
2730
) {
2831
this.findById = findById({ pool, logger });
2932
this.cache = new BentoCache({
3033
default: 'organizationAccessTokens',
34+
plugins: prometheusConfig.isEnabled
35+
? [
36+
prometheusPlugin({
37+
prefix: 'bentocache_organization_access_tokens',
38+
}),
39+
]
40+
: undefined,
3141
stores: {
3242
organizationAccessTokens: bentostore()
3343
.useL1Layer(
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable, Scope } from 'graphql-modules';
2+
3+
@Injectable({
4+
scope: Scope.Singleton,
5+
})
6+
export class PrometheusConfig {
7+
constructor(private _isEnabled = false) {}
8+
9+
get isEnabled() {
10+
return this._isEnabled;
11+
}
12+
}

packages/services/server/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { AuthN } from '../../api/src/modules/auth/lib/authz';
5858
import { OrganizationAccessTokenStrategy } from '../../api/src/modules/auth/lib/organization-access-token-strategy';
5959
import { SuperTokensUserAuthNStrategy } from '../../api/src/modules/auth/lib/supertokens-strategy';
6060
import { TargetAccessTokenStrategy } from '../../api/src/modules/auth/lib/target-access-token-strategy';
61+
import { OrganizationAccessTokenValidationCache } from '../../api/src/modules/auth/providers/organization-access-token-validation-cache';
6162
import { OrganizationAccessTokensCache } from '../../api/src/modules/organization/providers/organization-access-tokens-cache';
6263
import { createContext, internalApiRouter } from './api';
6364
import { asyncStorage } from './async-storage';
@@ -404,6 +405,7 @@ export async function main() {
404405
supportConfig: env.zendeskSupport,
405406
pubSub,
406407
appDeploymentsEnabled: env.featureFlags.appDeploymentsEnabled,
408+
prometheus: env.prometheus,
407409
});
408410

409411
const authN = new AuthN({
@@ -421,7 +423,10 @@ export async function main() {
421423
(logger: Logger) =>
422424
new OrganizationAccessTokenStrategy({
423425
logger,
424-
cache: registry.injector.get(OrganizationAccessTokensCache),
426+
organizationAccessTokensCache: registry.injector.get(OrganizationAccessTokensCache),
427+
organizationAccessTokenValidationCache: registry.injector.get(
428+
OrganizationAccessTokenValidationCache,
429+
),
425430
}),
426431
(logger: Logger) =>
427432
new TargetAccessTokenStrategy({

pnpm-lock.yaml

Lines changed: 24 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)