Skip to content

Commit 0932214

Browse files
arpendu11Arpendu Kumar Garai
andauthored
Feature Flags in Node.js Backend with LaunchDarkly (thombergs#204)
* nodejs-launchdarkly * segregating code examples * refactoring the code sections * refactor directory name * minor changes * addressed review comments * review comments * review comments Co-authored-by: Arpendu Kumar Garai <[email protected]>
1 parent d61382d commit 0932214

File tree

8 files changed

+364
-0
lines changed

8 files changed

+364
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import util from 'util';
2+
import express from 'express';
3+
import LaunchDarkly from 'launchdarkly-node-server-sdk';
4+
import Logger from './logger.js';
5+
6+
const PORT = 5000;
7+
const app = express();
8+
const simpleLogger = new Logger('SimpleLogging');
9+
10+
const LD_SDK_KEY = 'sdk-********-****-****-****-************';
11+
const LOG_LEVEL_FLAG_KEY = 'backend-log-level';
12+
const client = LaunchDarkly.init(LD_SDK_KEY);
13+
const asyncGetFlag = util.promisify(client.variation);
14+
15+
client.once('ready', async () => {
16+
const user = {
17+
anonymous: true
18+
};
19+
const initialLogLevel = await asyncGetFlag(LOG_LEVEL_FLAG_KEY, user, 'debug');
20+
Logger.setLogLevel(initialLogLevel);
21+
22+
app.get('/', (req, res) => {
23+
simpleLogger.debug('detailed debug message');
24+
simpleLogger.log('simple log message');
25+
simpleLogger.warn('Warning warning do something');
26+
simpleLogger.error('ERROR! ERROR!');
27+
res.sendStatus(200);
28+
});
29+
30+
app.listen(PORT, () => {
31+
simpleLogger.log(`Server listening on port ${PORT}`);
32+
});
33+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import cron from 'cron';
2+
import LaunchDarkly from 'launchdarkly-node-server-sdk';
3+
4+
const CronJob = cron.CronJob;
5+
const CronTime = cron.CronTime;
6+
7+
// Initiating LaunchDarkly Client
8+
const LD_SDK_KEY = 'sdk-********-****-****-****-************';
9+
const userName = 'admin';
10+
const launchDarklyClient = LaunchDarkly.init( LD_SDK_KEY );
11+
12+
launchDarklyClient.once('ready', async () => {
13+
const cronConfig = await launchDarklyClient.variation(
14+
'cron-config',
15+
{
16+
key: userName
17+
},
18+
'*/4 * * * *' // Default fall-back variation value.
19+
);
20+
21+
const job = new CronJob(cronConfig, function() {
22+
run();
23+
}, null, false)
24+
25+
let run = () => {
26+
console.log('scheduled task called');
27+
}
28+
29+
let scheduler = () => {
30+
console.log('CRON JOB STARTED WILL RUN AS PER LAUNCHDARKLY CONFIG');
31+
job.start();
32+
}
33+
34+
let schedulerStop = () => {
35+
job.stop();
36+
console.log('scheduler stopped');
37+
}
38+
39+
let schedulerStatus = () => {
40+
console.log('cron status ---->>>', job.running);
41+
}
42+
43+
let changeTime = (input) => {
44+
job.setTime(new CronTime(input));
45+
console.log('changed to every 1 second');
46+
}
47+
48+
scheduler();
49+
setTimeout(() => {schedulerStatus()}, 1000);
50+
setTimeout(() => {schedulerStop()}, 9000);
51+
setTimeout(() => {schedulerStatus()}, 10000);
52+
setTimeout(() => {changeTime('* * * * * *')}, 11000);
53+
setTimeout(() => {scheduler()}, 12000);
54+
setTimeout(() => {schedulerStop()}, 16000);
55+
}
56+
);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { format } from 'date-fns';
2+
import padEnd from 'lodash/padEnd.js';
3+
import capitalize from 'lodash/capitalize.js';
4+
5+
class DynamicLogger {
6+
constructor( module, ldClient, flagKey, user ) {
7+
this.module = module ? module : '';
8+
this.ldClient = ldClient;
9+
this.flagKey = flagKey;
10+
this.user = user;
11+
this.previousLevel = null;
12+
}
13+
14+
writeToConsole(level, message) {
15+
const dateTime = format(new Date(), 'MM-dd-yyyy HH:mm:ss:SSS');
16+
const formattedLevel = padEnd(capitalize(level), 5);
17+
const formattedMessage = `${dateTime} ${formattedLevel} [${
18+
this.module
19+
}] ${message}`;
20+
console[level](formattedMessage, '');
21+
}
22+
23+
async debug( message ) {
24+
if ( await this._presentLog( 'debug' ) ) {
25+
this.writeToConsole('debug', message);
26+
}
27+
}
28+
29+
async error( message ) {
30+
if ( await this._presentLog( 'error' ) ) {
31+
this.writeToConsole('error', message);
32+
}
33+
}
34+
35+
async info( message ) {
36+
if ( await this._presentLog( 'info' ) ) {
37+
this.writeToConsole('info', message);
38+
}
39+
}
40+
41+
async warn( message ) {
42+
if ( await this._presentLog( 'warn' ) ) {
43+
this.writeToConsole('warn', message);
44+
}
45+
}
46+
47+
async _presentLog( level ) {
48+
49+
const minLogLevel = await this.ldClient.variation(
50+
this.flagKey,
51+
{
52+
key: this.user
53+
},
54+
'debug' // Default/fall-back value if LaunchDarkly unavailable.
55+
);
56+
57+
if ( minLogLevel !== this.previousLevel ) {
58+
console.log( `Present log-level: ${ minLogLevel }` );
59+
}
60+
61+
switch ( this.previousLevel = minLogLevel ) {
62+
case 'error':
63+
return level === 'error';
64+
case 'warn':
65+
return level === 'error' || level === 'warn';
66+
case 'info':
67+
return level === 'error' || level === 'warn' || level === 'info';
68+
default:
69+
return true;
70+
}
71+
}
72+
}
73+
74+
export default DynamicLogger;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import chalk from 'chalk';
2+
import LaunchDarkly from 'launchdarkly-node-server-sdk';
3+
import DynamicLogger from './dynamic_logger.js';
4+
5+
const LD_SDK_KEY = 'sdk-********-****-****-****-************';
6+
const flagKey = 'backend-log-level';
7+
const userName = 'admin';
8+
const launchDarklyClient = LaunchDarkly.init( LD_SDK_KEY );
9+
let logger;
10+
let loop = 0;
11+
12+
launchDarklyClient.once('ready', async () => {
13+
setTimeout( executeLoop, 1000 );
14+
}
15+
);
16+
17+
async function executeLoop () {
18+
logger = new DynamicLogger( 'DynamicLogging', launchDarklyClient, flagKey, userName );
19+
console.log( chalk.dim.italic( `Loop ${ ++loop }` ) );
20+
logger.debug( 'Executing loop.' );
21+
logger.debug('This is a debug log.');
22+
logger.info('This is an info log.');
23+
logger.warn('This is a warn log.');
24+
logger.error('This is a error log.');
25+
setTimeout( executeLoop, 1000 );
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import LaunchDarkly from 'launchdarkly-node-server-sdk';
2+
import express from 'express';
3+
4+
const app = express();
5+
6+
app.get("/", async (req, res) => {
7+
const flags = await init();
8+
res.send(flags);
9+
});
10+
app.listen(8080);
11+
12+
const LD_SDK_KEY = 'sdk-********-****-****-****-************';
13+
const userName = 'admin';
14+
let client;
15+
16+
async function init() {
17+
if (!client) {
18+
client = LaunchDarkly.init(LD_SDK_KEY);
19+
await client.waitForInitialization();
20+
}
21+
22+
const user = {
23+
key: userName
24+
};
25+
const allFlagsState = await client.allFlagsState(user);
26+
const flags = allFlagsState.allValues();
27+
return flags;
28+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { format } from 'date-fns';
2+
import padEnd from 'lodash/padEnd.js';
3+
import capitalize from 'lodash/capitalize.js';
4+
5+
const LEVELS = { debug: 10, log: 20, warn: 30, error: 40 };
6+
let currentLogLevel = LEVELS['debug'];
7+
8+
class Logger {
9+
constructor(module) {
10+
this.module = module ? module : '';
11+
12+
this.debug = this.debug.bind(this);
13+
this.log = this.log.bind(this);
14+
this.warn = this.warn.bind(this);
15+
this.error = this.error.bind(this);
16+
this.writeToConsole = this.writeToConsole.bind(this);
17+
}
18+
19+
static setLogLevel(level) {
20+
currentLogLevel = LEVELS[level];
21+
}
22+
23+
static get(module) {
24+
return new Logger(module);
25+
}
26+
27+
writeToConsole(level, message, context = '') {
28+
if (LEVELS[level] >= currentLogLevel) {
29+
const dateTime = format(new Date(), 'MM-dd-yyyy HH:mm:ss:SSS');
30+
const formattedLevel = padEnd(capitalize(level), 5);
31+
const formattedMessage = `${dateTime} ${formattedLevel} [${
32+
this.module
33+
}] ${message}`;
34+
console[level](formattedMessage, context);
35+
}
36+
}
37+
38+
debug(message, context) {
39+
this.writeToConsole('debug', message, context);
40+
}
41+
42+
log(message, context) {
43+
this.writeToConsole('log', message, context);
44+
}
45+
46+
warn(message, context) {
47+
this.writeToConsole('warn', message, context);
48+
}
49+
50+
error(message, context) {
51+
this.writeToConsole('error', message, context);
52+
}
53+
}
54+
55+
export default Logger;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "nodejs-backend-feature-flag-launchdarkly",
3+
"version": "1.0.0",
4+
"description": "Feature Flags in Node.js backend server with LaunchDarkly",
5+
"main": "index.js",
6+
"type": "module",
7+
"scripts": {
8+
"start": "node index.js",
9+
"bootstrap": "node bootstrap.js",
10+
"dynamic": "node dynamic_logging.js",
11+
"rateLimiter": "node rate_limiter.js",
12+
"cron": "node cron_job.js"
13+
},
14+
"author": "Arpendu Kumar Garai",
15+
"license": "ISC",
16+
"dependencies": {
17+
"chalk": "4.1.2",
18+
"cors": "^2.8.5",
19+
"cron": "^2.1.0",
20+
"date-fns": "^2.28.0",
21+
"express": "^4.18.1",
22+
"express-rate-limit": "^6.5.1",
23+
"launchdarkly-node-server-sdk": "^6.4.2",
24+
"lodash": "^4.17.21"
25+
}
26+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import bodyParser from 'body-parser';
2+
import express from 'express';
3+
import cors from 'cors';
4+
import rateLimit from 'express-rate-limit';
5+
import LaunchDarkly from 'launchdarkly-node-server-sdk';
6+
import LdLogger from './ld_logger.js';
7+
8+
// Initiating LaunchDarkly Client
9+
const LD_SDK_KEY = 'sdk-d2432dc7-e56a-458b-9f93-0361af47d578';
10+
const userName = 'admin';
11+
const launchDarklyClient = LaunchDarkly.init( LD_SDK_KEY );
12+
13+
// Initiating the Logger
14+
const flagKey = 'backend-log-level';
15+
let logger;
16+
launchDarklyClient.once('ready', async () => {
17+
logger = new LdLogger( launchDarklyClient, flagKey, userName );
18+
serverInit();
19+
}
20+
);
21+
22+
const serverInit = async () => {
23+
24+
// Essential globals
25+
const app = express();
26+
27+
// Initialize global application middlewares
28+
app.use(cors());
29+
app.use(
30+
bodyParser.urlencoded({
31+
extended: true
32+
})
33+
);
34+
app.use(
35+
bodyParser.json({
36+
type: 'application/json'
37+
})
38+
);
39+
40+
// Initialize Rate Limit Midlleware
41+
const rateLimiterConfig = await launchDarklyClient.variation(
42+
'rate-limiter-config',
43+
{
44+
key: userName // The static "user" for this task.
45+
},
46+
{
47+
windowMs: 24 * 60 * 60 * 1000, // 24 hrs in milliseconds
48+
max: 100, // Limit each IP to 100 requests per `window`
49+
// (here, per 24 hours). Set it to 0 to disable rateLimiter
50+
message: 'You have exceeded 100 requests in 24 hrs limit!',
51+
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
52+
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
53+
} // Default/fall-back value
54+
);
55+
app.use(rateLimit(rateLimiterConfig));
56+
57+
// Initialize API
58+
app.get('/hello', function (req, res) {
59+
return res.send('Hello World')
60+
});
61+
62+
// Initialize server
63+
app.listen(5000, () => {
64+
logger.info('Starting server on port 5000');
65+
});
66+
};

0 commit comments

Comments
 (0)