Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add sentry dsn rotation logic #466

Merged
merged 10 commits into from Oct 7, 2023
36 changes: 31 additions & 5 deletions src/Config/CliBuilder.ts
@@ -1,8 +1,13 @@
import { CliConfig, ConfigReader } from './ConfigReader';
import { ClusterArgs, Helpers, logger, LogLevel } from '../Utils';
import { SystemConfigReader } from './SystemConfigReader';
import { CliInfo } from './CliInfo';
import { Arguments, Argv, CommandModule } from 'yargs';
import { runWithAsyncContext, setContext } from '@sentry/node';
import {
init as initSentry,
runWithAsyncContext as runSentryWithAsyncContext,
setContext as setSentryContext
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
} from '@sentry/node';

export interface CliBuilderOptions {
info: CliInfo;
Expand Down Expand Up @@ -89,12 +94,33 @@ export class CliBuilder {
.reduce((acc: Argv, item: CommandModule) => {
const handler = item.handler.bind(item);

item.handler = (args) =>
runWithAsyncContext(() => {
setContext('args', args);
item.handler = async (args: Arguments) => {
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
const systemConfigReader = new SystemConfigReader(args.api as string);
const systemConfig = await systemConfigReader.read();

handler(args);
initSentry({
attachStacktrace: true,
dsn: systemConfig.sentryDsn,
release: process.env.VERSION,
beforeSend: (event) => {
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
if (event.contexts.args) {
event.contexts.args = {
...event.contexts.args,
t: event.contexts.args.t && '[Filtered]',
token: event.contexts.args.token && '[Filtered]'
};
}

return event;
}
});

return runSentryWithAsyncContext(() => {
setSentryContext('args', args);

return handler(args);
});
};

acc.command(item);

Expand Down
153 changes: 153 additions & 0 deletions src/Config/SystemConfigReader.ts
@@ -0,0 +1,153 @@
import { logger } from '../Utils';
import request, { RequestPromiseAPI } from 'request-promise';
import { promises, constants } from 'fs';
import { join } from 'path';
import { homedir } from 'os';

const { readFile, writeFile, access } = promises;
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
const { F_OK } = constants;

export interface SystemConfig {
sentryDsn?: string;
}

interface SystemConfigFile {
data: SystemConfig;
updatedAt: Date;
}

export class SystemConfigReader {
private readonly rotationInterval = 3600000;
private readonly path = join(homedir(), '.brightclirc');
private client: RequestPromiseAPI;
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved

constructor(baseUrl: string) {
this.client = request.defaults({
baseUrl,
timeout: 1500,
json: true
});
}

public async read(): Promise<SystemConfig> {
await this.rotateIfNecessary();
const configFile = await this.getConfigFile();

return {
sentryDsn: process.env['SENTRY_DSN'],
...configFile.data
};
}

private async rotateIfNecessary() {
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
if (process.env.NODE_ENV !== 'production') {
return;
}

logger.debug('Trying to rotate system config');

const configFile = await this.getConfigFile();
const lifeTime = Date.now() - configFile.updatedAt.getTime();

if (lifeTime < this.rotationInterval) {
logger.debug(
'Rotation is not needed, system config is fresh: %s ms',
lifeTime
);

return;
}

logger.debug(
"Rotating system config because it's too old: %s ms",
lifeTime
);

const newConfig = await this.fetchNewConfig();

if (newConfig) {
await this.updateConfigFile({
data: newConfig,
updatedAt: new Date()
});
} else {
logger.debug('Rotation failed');

await this.updateConfigFile({
...configFile,
updatedAt: new Date()
});
Comment on lines +79 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems this is redundant. Should we update the configuration as though the rotation was successful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yes, just because if we don't do that, it will try to perform rotation on every bright-cli call causing at most 1.5 seconds delay.

Since rotation logic is not crucial for the cli for now, I think it's ok in case of failure to repeat it later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you wish. However, I'm still uncertain about how it is supposed to work for long-running processes like the repeater which is intended to work for months.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a separate PR for the background rotation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lsndr please open a ticket in Jira

}
}

private defaultConfigFile(): SystemConfigFile {
return {
data: {},
updatedAt: new Date()
};
}

private async getConfigFile() {
const defaultConfigFile = this.defaultConfigFile();

try {
logger.debug('Loading system config file');

const configFileExists = await this.configFileExists();

if (!configFileExists) {
logger.debug(
"System config file doesn't exist. Creating a new one with default values"
);

await this.updateConfigFile(defaultConfigFile);
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
}

const file = await readFile(this.path);
const fileConfig = JSON.parse(file.toString()) as SystemConfigFile;
fileConfig.updatedAt = new Date(fileConfig.updatedAt);

return fileConfig;
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
logger.debug('Error during loading system config file', e);
}

logger.debug('Using default system config file');

return defaultConfigFile;
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
}

private async updateConfigFile(configFile: SystemConfigFile) {
logger.debug('Updating system config file');

try {
await writeFile(this.path, JSON.stringify(configFile));
} catch (e) {
logger.debug('Error during updating system config file', e);
}
}

private async configFileExists(): Promise<boolean> {
try {
await access(this.path, F_OK);

return true;
} catch (e) {
// pass
}

return false;
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
}

private async fetchNewConfig(): Promise<SystemConfig | undefined> {
logger.debug('Fetching new system config');

try {
return await this.client.get({
uri: '/api/v1/cli/config'
});
} catch (e) {
logger.debug('Error during fetching new system config: ', e);
}
}
}
20 changes: 0 additions & 20 deletions src/Utils/Sentry.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/Utils/index.ts
Expand Up @@ -3,4 +3,3 @@ export * from './Helpers';
export * from './Logger';
export * from './Backoff';
export * from './Traceroute';
export * from './Sentry';
3 changes: 0 additions & 3 deletions src/index.ts
Expand Up @@ -16,9 +16,6 @@ import {
Integration
} from './Commands';
import { CliBuilder, container } from './Config';
import { sentry } from './Utils';

sentry();

container.resolve(CliBuilder).build({
commands: [
Expand Down