visit
def greeter():
greeterLanguageFrench = true # Comment to print greeting in English
# greeterLanguageFrench = false # Un comment to print greeting in English
if ( greeterLanguageFrench )
return "Bonjour monde!"
else
return "Hello World!"
I hope you get the picture of how we’re changing the code path using if/else statement and commenting/uncommenting. However, the feature flag technique doesn’t require you to implement it in this way. Instead, you can control the flags remotely and turn them on/off without even changing the code in production. And this helps us . Decoupling deployment from release helps where the team is supposed to build a feature that requires weeks of work, leading to a long-lived feature branch. This long-lived branch comes with its own complexity of merging and releasing.
TBD is a source-control branching model, where developers collaborate on code in a single branch called ‘trunk’ *, resist any pressure to create other long-lived development branches by employing documented techniques. They, therefore, avoid merge hell, do not break the build, and live happily ever after.
Feature flag allows us to ship our code to production in smaller commits and deploy in a dormant state. Now, you can decide when to turn it on/off, get feedback and iterate over it. Let’s go through some scenarios which can help us understand why we need feature flags.
Maintenance - Keep track of long-lived feature flags in your existing codebase so new flags don’t conflict with old ones.
Ownership- One must own the lifecycle of a flag from addition to removal; otherwise, over time, flags add up.
Flag names - Names should describe what they do in minimal words and should follow common naming convention throughout the codebase.
Audit history - If someone is turning a flag “on” or “off”, make sure that you know who it is.
def func(config):
if(config.is_feature_on):
# do something
else:
# do something else
Feature flags can be a day savior, but they can turn into a disaster like what happened on 1 Aug 2012, and it cost that day.
So, more importantly, we need a better feature flag management tool in place. Instead of , we can adopt any existing feature flag management platform like , which provides a SaaS platform to manage feature flags and help us simplify implementation through . Apart from LaunchDarkly, we do have alternative open-source tools. I’ve listed some of them below.
To begin with, you need a LaunchDarkly account for this demo, and you can create a trial account . Once you log in, you will see the Feature Flag list on the left side of the panel.
For this demo, you need an and a . Both of these are available under Account Settings > Projects. You need to click on the project’s name to see the available environment and associated keys. Copy keys of the Test environment for this demo.
We will use those keys to run our demo application locally. You can find the instructions on “How to run locally” in the DEMO application .
We will need these keys to interact with for Python and . SDK should be implemented in a singleton pattern rather than creating multiple instances. So we need one instance of SDK throughout our Flask application. Let’s look at the basic implementation I followed.
def setup_ld_client(app) -> ldclient.LDClient:
featureStore = InMemoryFeatureStore()
LD_SDK_KEY = app.config["LD_SDK_KEY"]
LD_FRONTEND_KEY = app.config["LD_FRONTEND_KEY"]
ld_config = LdConfig(
sdk_key=LD_SDK_KEY,
http=HTTPConfig(connect_timeout=30, read_timeout=30),
feature_store=featureStore,
inline_users_in_events=True
)
client = ldclient.LDClient(config=ld_config)
return client
A workflow that progressively rolls out a flag over time.
To add that feature flag to the LaunchDarkly account, go to Feature Flags on the left side panel. Click Create Flag and fill these values in.
Note: are flag values to serve based on .
To enable it, go to the feature dark-theme-button -> Setting tab -> Client-side SDK availability -> Checkbox SDKs using Client-side ID
and Save changes.
<script crossorigin="anonymous" src="//unpkg.com/launchdarkly-js-client-sdk@2"></script>
<script>
var ldclient = LDClient.initialize("{{ config['LD_FRONTEND_KEY'] }}", {{ user_context | safe }}, options = {
bootstrap: {{ all_flags | safe }}
});
var renderButton = function() {
var showFeature = ldclient.variation("dark-theme-button", false);
var displayWidget = document.getElementById('dark-theme-button');
if (displayWidget) {
if (showFeature) {
displayWidget.style.display = "block";
} else {
displayWidget.style.display = "none";
}
}
ldclient.waitForInitialization().then(function() {
renderButton();
})
ldclient.on('change', function() {
renderButton();
});
}
</script>
Now, test the feature flag you just created. Once you toggle it on. There will be a button on the left corner.
If you toggle that button, it should appear like this.
Yes, you can add a flag that can define the logger level before any requests come in, and you can operate it remotely. Flask API provides us with before_request to register any function, and it will run before each request. See the below example.
@app.before_request
def setLoggingLevel():
from flask import request
logLevel = app.ldclient.variation(
"set-logging-level",
get_ld_non_human_user(request),
logging.INFO)
app.logger.info(f"Log level: {logLevel}")
app.logger.setLevel(logLevel)
logging.getLogger("werkzeug").setLevel(logLevel)
logging.getLogger().setLevel(logLevel)
Note: In the above, I’m providing three things to ldclient.variation()
: 1. Flag key 2. User context 3. Default value.
To add that feature flag to the LaunchDarkly account, go to Feature Flags on the left side panel.
Click Create Flag and Fill these values in.
Note: Make sure every feature flag should be in the same environment as the keys you used to set up LaunchDarkly Client in the application.
Now, go to //localhost:5000/
and see the logs in the terminal of your running application.
127.0.0.1 - - [29/Sep/2022 14:49:47] "GET / HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [29/Sep/2022 14:49:47] "GET / HTTP/1.1" 200 -
[2022-09-29 14:49:47,972] INFO in run: Log level: 20
INFO:arun:Log level: 20
127.0.0.1 - - [29/Sep/2022 14:49:47] "GET /static/css/custom.css HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [29/Sep/2022 14:49:47] "GET /static/css/custom.css HTTP/1.1" 200 -
[2022-09-29 14:49:47,983] INFO in run: Log level: 20
INFO:app.run:Log level: 20
127.0.0.1 - - [29/Sep/2022 14:49:47] "GET /static/js/dark-mode.js HTTP/1.1" 304 -
INFO:werkzeug:127.0.0.1 - - [29/Sep/2022 14:49:47] "GET /static/js/dark-mode.js HTTP/1.1" 304 -
[2022-09-29 14:49:48,848] INFO in run: Log level: 20
INFO:app.run:Log level: 20
127.0.0.1 - - [29/Sep/2022 14:49:48] "GET /favicon.ico HTTP/1.1" 404 -
INFO:werkzeug:127.0.0.1 - - [29/Sep/2022 14:49:48] "GET /favicon.ico HTTP/1.1" 404 -
Now, recheck the logs by going to the homepage of the local application.
INFO:werkzeug:127.0.0.1 - - [29/Sep/2022 14:55:11] "GET / HTTP/1.1" 200 -
DEBUG:root:{'key': 'sudhanshu', 'ip': '127.0.0.1', 'email': '[email protected]', 'custom': {'type': 'machine'}}
[2022-09-29 14:55:12,018] INFO in run: Log level: 10
INFO:app.run:Log level: 10
127.0.0.1 - - [29/Sep/2022 14:55:12] "GET /static/js/dark-mode.js HTTP/1.1" 304 -
INFO:werkzeug:127.0.0.1 - - [29/Sep/2022 14:55:12] "GET /static/js/dark-mode.js HTTP/1.1" 304 -
DEBUG:root:{'key': 'sudhanshu', 'ip': '127.0.0.1', 'email': '[email protected]', 'custom': {'type': 'machine'}}
[2022-09-29 14:55:12,027] INFO in run: Log level: 10
INFO:app.run:Log level: 10
Context: API team developer wants to add a new field in API response, i.e., count
. This field will help end users get a count of the number of products returned in an API response. Now, the API team lead decided first to validate latency in API response, whether it is in a reasonable range, and roll it out to a few beta users so that they can get their feedback before rolling it out to everyone.
You can see how I’m evaluating a feature flag using ldclient to get a current variation of a flag with a default value. Just for the sake of simplicity, this is how I’m implementing this in the Flask application.
@api.route('/fashion', methods=['GET'])
@token_required
def list_fashion(current_user):
# add a additional field in api response with feature flag
try:
query_result = Products.query.filter_by(
product_type='fashion').all()
product_schema = ProductSchema(many=True)
data = {
"message": "successfully retrieved all products",
"data": product_schema.dump(query_result)
}
# Feature flag to add a field in api response
if current_app.ldclient.variation(
'add-field-total',
current_user.get_ld_user(), False):
data.update({'count': len(data)})
return jsonify(data)
except Exception as e:
current_app.logger.debug(
f'Something went wrong: {e}',
exc_info=True)
return jsonify({
"message": "failed to retrieve all products",
"error": str(e),
"data": None
}), 500
Before hitting the request, you need to generate an API token of our application. To generate one, use this curl command:
curl --location --request POST 'localhost:5000/api/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email" : "[email protected]",
"password" : "12345"
}'
Once you run that command copy the token value, we will need it in further steps. Now, see the response using the below curl command. You’ll see there is no count in the API response.
curl --location --request GET 'localhost:5000/api/fashion' \
--header 'Authorization: token PUT_TOKEN_HERE’
Response:
{
"data": [...],
"message": "successfully retrieved all products"
}
Now, we will create a feature flag in LaunchDarkly using the same flow as we did earlier. Using these Values.
Before we turn on that created feature flag, let’s talk about the dialog box we see whenever we turn in any feature flag toggle.
To use targeting, you need to go into Feature Flag -> <Feature Flag Name> -> Targeting tab.
Create any user from this page and add this user in Feature flag-> Feature Name -> Individual targeting section. One user in True
Variation and another user in False
Variation.
Now, before calling this , you need to create a token for this user as well. And use the same curl command to get a list of products we used in the earlier step.
Make an API call using those commands for two different users. You will see API is returning two different schemas of response. One contains count
, and the other doesn’t because you only released that feature to one user; for the other user it is still the same.
In this case, you’re just disabling registration for a few minutes, allowing registered users first. You might be wondering is this kind of control behavior possible where no new user register for some time? Yes, It is. See the below code. I’ve created a flag called disable-registration; the default value is false. And once you turn it on, it will redirect all users back home with a message.
@core.route('/register', methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("core.index"))
if current_app.ldclient.variation('disable-registration',
current_user.get_ld_user(), False):
flash("Not accepting new registration, try after sometime")
return redirect(url_for("core.index"))
if request.method == "POST":
user = User(email=request.form["userEmail"])
if User.query.filter_by( email=request.form["userEmail"]).first() is not None:
flash("Email is already taken. Please choose another email")
return redirect(url_for("core.register"))
if request.form["inputPassword"] != request.form["confirmPassword"]:
flash("Passwords must match")
return redirect(url_for("core.register"))
user.set_password(request.form["inputPassword"])
db.session.add(user)
db.session.commit()
flash("Congratulations, you are now a registered user!")
login_user(user)
return redirect(url_for("core.dashboard"))
return render_template('register.html')
Follow the same steps as we did earlier to create a feature flag. Use provided values.
That’s about it in this blog post. Feel free to reach out to