IBM Cloud App Configuration SDK is used to perform feature flag and property evaluation and track custom metrics for Experimentation based on the configuration on IBM Cloud App Configuration service.
IBM Cloud App Configuration is a centralized feature management and configuration service on IBM Cloud for use with web and mobile applications, microservices, and distributed environments.
Instrument your applications with App Configuration Rust SDK, and use the App Configuration dashboard, CLI or API to define feature flags or properties, organized into collections and targeted to segments. Toggle feature flag states in the cloud to activate or deactivate features in your application or environment, when required. Run experiments and measure the effect of feature flags on end users by tracking custom metrics. You can also manage the properties for distributed applications centrally.
Add the SDK to your Cargo.toml:
[dependencies]
ibm-appconfiguration-rust-sdk = "0.1.0-rc.0"use ibm_appconfiguration_rust_sdk::{
AppConfiguration, AppConfigurationContextOptions,
ConfigurationProvider, Entity, Value,
};Initialize the SDK to connect with your App Configuration service instance.
use ibm_appconfiguration_rust_sdk::{
AppConfiguration, AppConfigurationContextOptions,
ConfigurationProvider, Entity, Value,
};
let region = "us-south";
let guid = "<guid>";
let apikey = "<apikey>";
let collection_id = "airlines-webapp";
let environment_id = "dev";
let mut client = AppConfiguration::new();
client.init(®ion, &guid, &apikey)?;
client.set_context(
&collection_id,
&environment_id,
AppConfigurationContextOptions::default(),
)?;
// Block until the background thread has fetched configuration from the server
client.wait_until_online();
⚠️ It is expected that initialization to be done only once.
After the SDK is initialized successfully, the feature flags & properties can be retrieved using the client as shown in the below code snippet.
Expand to view the example snippet
// Get feature
match client.get_feature("online-check-in") {
Ok(feature) => {
let result = feature.get_current_value(&entity)?;
println!("{:?}", result.value);
}
Err(e) => eprintln!("Feature not found: {e}"),
}
// Get property
match client.get_property("check-in-charges") {
Ok(property) => {
let result = property.get_current_value(&entity)?;
println!("{:?}", result.value);
}
Err(e) => eprintln!("Property not found: {e}"),
}where,
- region: Region name where the App Configuration service instance is created.
See list of supported locations here. E.g.
us-south,au-syd,eu-gb,us-east,eu-de,ca-tor,jp-tok,jp-osaetc. - guid: Instance Id of the App Configuration service. Obtain it from the service credentials section of the App Configuration dashboard.
- apikey: ApiKey of the App Configuration service. Obtain it from the service credentials section of the App Configuration dashboard.
- collection_id: Id of the collection created in App Configuration service instance under the Collections section.
- environment_id: Id of the environment created in App Configuration service instance under the Environments section.
Set the SDK to connect to App Configuration service by using a private endpoint that is accessible only through the IBM Cloud private network.
client.use_private_endpoint(true);This must be done before calling init() on the SDK.
In order for your application and SDK to continue its operations even during the unlikely scenario of App Configuration service across your application restarts, you can configure the SDK to work using a persistent cache. The SDK uses the persistent cache to store the App Configuration data that will be available across your application restarts.
use std::path::PathBuf;
use ibm_appconfiguration_rust_sdk::AppConfigurationContextOptions;
let options = AppConfigurationContextOptions::try_new(
Some(PathBuf::from("/var/lib/myapp/cache")),
None,
true,
)?;
client.set_context(&collection_id, &environment_id, options)?;- persistent_cache_directory: Absolute path to a directory which has read & write permission for the user. The SDK will create a file —
appconfiguration.jsonin the specified directory, and it will be used as the persistent cache to store the App Configuration service information.
When persistent cache is enabled, the SDK will keep the last known good configuration at the persistent cache. In the case of App Configuration server being unreachable, the latest configurations at the persistent cache is loaded to the application to continue working.
Please ensure that the cache file is not lost or deleted in any case. For example, consider the case when a Kubernetes pod is restarted and the cache file (appconfiguration.json) was stored in an ephemeral volume of the pod. As the pod gets restarted, Kubernetes destroys the ephemeral volume, and the cache file gets deleted. Make sure that the cache file created by the SDK is always stored in a persistent volume by providing the correct absolute path of the persistent directory.
The SDK is also designed to serve configurations, perform feature flag & property evaluations without being connected to App Configuration service.
use std::path::PathBuf;
use ibm_appconfiguration_rust_sdk::AppConfigurationContextOptions;
let options = AppConfigurationContextOptions::try_new(
None,
Some(PathBuf::from("saflights/flights.json")),
false,
)?;
client.set_context(&collection_id, &environment_id, options)?;This will return an error if the given bootstrap_file is not found or if the JSON cannot be parsed.
- bootstrap_file: Absolute path of the JSON file which contains configuration details. Make sure to provide a valid JSON file. You can generate this file using the
ibmcloud ac exportcommand of the IBM Cloud App Configuration CLI. - live_config_update_enabled: Live configuration update from the server. Set this value to
falseif new configuration values should not be fetched from the server.
match client.get_feature("online-check-in") {
Ok(feature) => {
println!("Feature Name: {}", feature.get_feature_name()?);
println!("Feature Id: {}", feature.get_feature_id()?);
println!("Feature Type: {}", feature.get_feature_data_type()?);
if feature.is_enabled()? {
// feature flag is enabled
} else {
// feature flag is disabled
}
}
Err(e) => eprintln!("Invalid feature id: {e}"),
}let feature_ids = client.get_feature_ids()?;
for id in &feature_ids {
if let Ok(feature) = client.get_feature(id) {
println!("Feature Name: {}", feature.get_feature_name()?);
println!("Feature Id: {}", feature.get_feature_id()?);
println!("Feature Type: {}", feature.get_feature_data_type()?);
println!("Is feature enabled? {}", feature.is_enabled()?);
}
}Use the feature.get_current_value(&entity) method to evaluate the value of the feature flag. This method returns a FeatureEvaluationResult containing the evaluated value, enabled status and evaluation details.
use std::collections::HashMap;
use ibm_appconfiguration_rust_sdk::{Entity, Value};
struct MyEntity {
id: String,
city: String,
country: String,
}
impl Entity for MyEntity {
fn get_id(&self) -> String { self.id.clone() }
fn get_attributes(&self) -> HashMap<String, Value> {
HashMap::from([
("city".to_string(), Value::from(self.city.clone())),
("country".to_string(), Value::from(self.country.clone())),
])
}
}
let entity = MyEntity {
id: "john_doe".to_string(),
city: "Bangalore".to_string(),
country: "India".to_string(),
};
let feature = client.get_feature("online-check-in")?;
let result = feature.get_current_value(&entity)?;
println!("{:?}", result.value); // Evaluated value (Boolean, Numeric, or String)
println!("{}", result.is_enabled); // Enabled status
println!("{:?}", result.details); // Detailed evaluation info
// result.details fields:
println!("{}", result.details.value_type); // e.g. "DISABLED_VALUE"
println!("{}", result.details.reason); // e.g. "Disabled value of the feature flag since the feature flag is disabled."
println!("{:?}", result.details.segment_name); // Some("segment_name") or None
println!("{:?}", result.details.rollout_percentage_applied); // Some(true/false) or None- entity_id: Id of the Entity. This will be a string identifier related to the Entity against which the feature is evaluated. For example, an entity might be an instance of an app that runs on a mobile device, a microservice that runs on the cloud, or a component of infrastructure that runs that microservice. For any entity to interact with App Configuration, it must provide a unique entity ID.
- entity_attributes: A
HashMapconsisting of the attribute name and their values that defines the specified entity. This is optional if the feature flag is not configured with any targeting definition. If targeting is configured, then entity attributes should be provided for the rule evaluation. An attribute is a parameter used to define a segment. The SDK uses the attribute values to determine if the specified entity satisfies the targeting rules, and returns the appropriate feature flag value.
Record custom metrics for experiments using the track method. Calling track will queue the metric event, which will be sent in batches to the App Configuration servers.
use ibm_appconfiguration_rust_sdk::AppConfigurationClient;
client.track("event_key", "entity_id")?;where:
- event_key: The event key for the metric associated with the running experiment. The event key in your metric and the event key in your code must match exactly.
Note: Custom metric tracking (
track) is currently a no-op placeholder. It will be replaced with a real implementation in a future release.
match client.get_property("check-in-charges") {
Ok(property) => {
println!("Property Name: {}", property.get_property_name()?);
println!("Property Id: {}", property.get_property_id()?);
println!("Property Type: {}", property.get_property_data_type()?);
}
Err(e) => eprintln!("Invalid property id: {e}"),
}let property_ids = client.get_property_ids()?;
for id in &property_ids {
if let Ok(property) = client.get_property(id) {
println!("Property Name: {}", property.get_property_name()?);
println!("Property Id: {}", property.get_property_id()?);
println!("Property Type: {}", property.get_property_data_type()?);
}
}Use the property.get_current_value(&entity) method to evaluate the value of the property. This method returns a PropertyEvaluationResult containing the evaluated value and evaluation details.
let entity = MyEntity {
id: "john_doe".to_string(),
city: "Bangalore".to_string(),
country: "India".to_string(),
};
let property = client.get_property("check-in-charges")?;
let result = property.get_current_value(&entity)?;
println!("{:?}", result.value); // Evaluated value (Boolean, Numeric, or String)
println!("{:?}", result.details); // Detailed evaluation info
// result.details fields:
println!("{}", result.details.value_type); // e.g. "DEFAULT_VALUE"
println!("{}", result.details.reason); // e.g. "Default value of the property."
println!("{:?}", result.details.segment_name); // Some("segment_name") or None- entity_id: Id of the Entity. This will be a string identifier related to the Entity against which the property is evaluated.
- entity_attributes: A
HashMapconsisting of the attribute name and their values that defines the specified entity. This is optional if the property is not configured with any targeting definition.
The SDK maintains a persistent WebSocket connection to the server. When you change a flag or property value in the IBM Cloud App Configuration dashboard, the server sends a change notification over that socket. The SDK re-fetches the full configuration and fires a RuntimeEventKind::RefreshSuccess event to every registered listener.
How it works:
| Server message | SDK action |
|---|---|
"test message" |
Keep-alive heartbeat — ignored, no fetch |
Any other text (e.g. "collection_id:c1;environment_id:e1") |
Config-change notification — fetch + fire RefreshSuccess |
Call emitter() to obtain the RuntimeEventEmitter, then attach a closure with on(). Listeners are called on the SDK background thread.
⚠️ Register your listener BEFOREset_context().set_context()spawns the background thread and seeds it with a clone of the emitter.Connectedand the firstRefreshSuccessboth fire beforewait_until_online()returns — a listener registered afterwards will miss them.
use std::sync::Arc;
use ibm_appconfiguration_rust_sdk::{
AppConfiguration, AppConfigurationContextOptions,
RuntimeEvent, RuntimeEventKind,
};
let mut client = AppConfiguration::new();
client.init(®ion, &guid, &apikey)?;
// Register the listener BEFORE set_context()
client.emitter().on(Arc::new(|event: RuntimeEvent| {
if event.kind == RuntimeEventKind::RefreshSuccess {
println!("Configuration changed — re-evaluate your flags!");
}
}))?;
// Background thread starts here; listener is already wired in
client.set_context(&collection_id, &environment_id, AppConfigurationContextOptions::default())?;
client.wait_until_online();Listener closures must be 'static + Send + Sync, so they cannot borrow the client directly. Wrap the client in Arc<Mutex<AppConfiguration>> and clone the Arc into the closure.
The critical ordering rule is:
- Lock →
init()→ unlock emitter().on(...)— lock-free (emitter()returns a cheapArcclone)- Lock →
set_context()→ unlock ← background thread spawned here with listener already seeded wait_until_online()
use std::sync::{Arc, Mutex};
use std::thread;
use ibm_appconfiguration_rust_sdk::{
AppConfiguration, AppConfigurationContextOptions,
ConfigurationProvider, RuntimeEvent, RuntimeEventKind,
};
let client = Arc::new(Mutex::new(AppConfiguration::new()));
// Step 1 — credentials
{
let mut c = client.lock().unwrap();
c.init(®ion, &guid, &apikey)?;
} // lock released
// Step 2 — register listener (emitter() is lock-free)
let listener_client = Arc::clone(&client);
client.lock().unwrap().emitter().on(Arc::new(move |event: RuntimeEvent| {
if event.kind != RuntimeEventKind::RefreshSuccess {
return;
}
// Acquire the client to read the freshly-fetched configuration
if let Ok(c) = listener_client.lock() {
match c.get_feature("online-check-in") {
Ok(f) => println!("online-check-in enabled = {:?}", f.is_enabled()),
Err(e) => eprintln!("error: {e}"),
}
}
}))?;
// Step 3 — bind context (spawns background thread with listener already wired)
{
let mut c = client.lock().unwrap();
c.set_context(&collection_id, &environment_id, AppConfigurationContextOptions::default())?;
} // lock released
// Step 4 — block until the first config fetch completes
client.lock().unwrap().wait_until_online();
// Main thread: park and let the listener handle all output
loop { thread::park(); }
⚠️ Do not callget_feature()/get_property()while holding the sameArc<Mutex<AppConfiguration>>lock that the listener also tries to acquire. The listener runs on the background thread. If the main thread holds the lock when the background thread fires the event, both threads deadlock waiting for each other. In the pattern above this is safe becausewait_until_online()releases the guard at the;(it is a temporary), and the main thread then callsthread::park()— it never holds the lock when the listener fires.
use std::sync::Arc;
use ibm_appconfiguration_rust_sdk::{RuntimeEvent, RuntimeEventKind};
client.emitter().on(Arc::new(|event: RuntimeEvent| {
match event.kind {
RuntimeEventKind::Connected => println!("[ws] connected to server"),
RuntimeEventKind::Disconnected => println!("[ws] disconnected — {:?}", event.status.offline_reason),
RuntimeEventKind::Closed => println!("[ws] server closed the connection"),
RuntimeEventKind::HeartbeatTimeout => println!("[ws] heartbeat timeout — reconnecting"),
RuntimeEventKind::RefreshSuccess => println!("[config] new configuration fetched ✓"),
RuntimeEventKind::RefreshFailure => eprintln!("[config] fetch failed — {:?}", event.status),
}
}))?;| Kind | When it fires |
|---|---|
Connected |
WebSocket handshake succeeded |
Disconnected |
Connection lost (network error, server restart, etc.) |
Closed |
Server sent a clean WebSocket close frame |
HeartbeatTimeout |
No "test message" heartbeat received within 65 s |
RefreshSuccess |
Config-change notification received and new config fetched successfully |
RefreshFailure |
Config-change notification received but HTTP fetch failed |
Any type that provides an ID and a map of attributes can be used with the client:
use std::collections::HashMap;
use ibm_appconfiguration_rust_sdk::{Entity, Value};
struct MyUser {
id: String,
city: String,
}
impl Entity for MyUser {
fn get_id(&self) -> String {
self.id.clone()
}
fn get_attributes(&self) -> HashMap<String, Value> {
HashMap::from([
("city".to_string(), Value::from(self.city.clone())),
])
}
}App Configuration service allows configuring the feature flag and properties in the following data types: Boolean, Numeric, String. The String data type can be of the format of a text string, JSON or YAML. The SDK processes each format accordingly as shown in the below table.
View Table
| Feature or Property value | DataType | DataFormat | Type of data returned by get_current_value().value |
Example output |
|---|---|---|---|---|
true |
BOOLEAN | not applicable | Value::Boolean |
true |
25 |
NUMERIC | not applicable | Value::Int64 / Value::Float64 |
25 |
"a string text" |
STRING | TEXT | Value::String |
"a string text" |
{"firefox":{"name":"Firefox"}} |
STRING | JSON | Value::String (raw JSON string) |
"{\"firefox\":{\"name\":\"Firefox\"}}" |
men:\n - John Smith |
STRING | YAML | Value::String (raw YAML string) |
"men:\n - John Smith" |
Feature flag
let feature = client.get_feature("json-feature")?;
println!("{}", feature.get_feature_data_type()?); // STRING
println!("{:?}", feature.get_feature_data_format()?); // Some("JSON")
let result = feature.get_current_value(&entity)?;
// result.value is Value::String containing raw JSON
if let Value::String(raw_json) = result.value {
let parsed: serde_json::Value = serde_json::from_str(&raw_json)?;
println!("{}", parsed["key"]);
}
let yaml_feature = client.get_feature("yaml-feature")?;
println!("{}", yaml_feature.get_feature_data_type()?); // STRING
println!("{:?}", yaml_feature.get_feature_data_format()?); // Some("YAML")
let result = yaml_feature.get_current_value(&entity)?;Property
let property = client.get_property("json-property")?;
println!("{}", property.get_property_data_type()?); // STRING
println!("{:?}", property.get_property_data_format()?); // Some("JSON")
let result = property.get_current_value(&entity)?;
if let Value::String(raw_json) = result.value {
let parsed: serde_json::Value = serde_json::from_str(&raw_json)?;
println!("{}", parsed["key"]);
}
let yaml_property = client.get_property("yaml-property")?;
println!("{}", yaml_property.get_property_data_type()?); // STRING
println!("{:?}", yaml_property.get_property_data_format()?); // Some("YAML")
let result = yaml_property.get_current_value(&entity)?;AppConfigurationContextOptions controls offline resilience and configuration update behaviour:
| Field | Type | Default | Description |
|---|---|---|---|
live_config_update_enabled |
bool |
true |
Poll/subscribe for live configuration updates from the server |
persistent_cache_directory |
Option<PathBuf> |
None |
Directory where a local appconfiguration.json cache file is written; used as fallback when the server is unreachable |
bootstrap_file |
Option<PathBuf> |
None |
Path to a static .json file to use as the initial (and only) configuration source when live_config_update_enabled is false |
Use AppConfigurationContextOptions::try_new() to construct and validate options at the same time:
use std::path::PathBuf;
use ibm_appconfiguration_rust_sdk::AppConfigurationContextOptions;
// Persist a cache and receive live updates
let options = AppConfigurationContextOptions::try_new(
Some(PathBuf::from("/var/cache/myapp")),
None,
true,
)?;
client.set_context(&collection_id, &environment_id, options)?;// Fully offline: serve only from a bundled JSON file
let options = AppConfigurationContextOptions::try_new(
None,
Some(PathBuf::from("config/bootstrap.json")),
false,
)?;
client.set_context(&collection_id, &environment_id, options)?;// Shut down background threads; preserve the on-disk cache
client.clean_up()?;
// Shut down background threads and delete the on-disk cache
client.clean_up_with_cache_clear()?;Once the SDK is initialized and wrapped in an Arc<Mutex<>>, the client can be obtained across other modules as shown below:
use std::sync::{Arc, Mutex};
use ibm_appconfiguration_rust_sdk::{AppConfiguration, ConfigurationProvider, Feature};
// Pass the Arc<Mutex<AppConfiguration>> across module boundaries
fn use_client(client: Arc<Mutex<AppConfiguration>>) {
let c = client.lock().unwrap();
if let Ok(feature) = c.get_feature("online-check-in") {
let enabled = feature.is_enabled().unwrap_or(false);
println!("online-check-in enabled: {enabled}");
}
}Try this sample application in the examples folder to learn more about feature and property evaluation.
This SDK requires connectivity to the internet (if bootstrap-based initialization is not done). The endpoints listed below should be reachable from the host/infrastructure where this SDK will run.
https://cloud.ibm.com:443
https://iam.cloud.ibm.com:443
https://{region}.apprapp.cloud.ibm.com:443
wss://{region}.apprapp.cloud.ibm.com:443
If opted for private endpoint by setting client.use_private_endpoint(true) then the allowlist will be:
https://cloud.ibm.com:443
https://private.iam.cloud.ibm.com:443
https://private.{region}.apprapp.cloud.ibm.com:443
wss://private.{region}.apprapp.cloud.ibm.com:443
where region is the region where your App Configuration service instance is provisioned such as us-south, us-east, eu-gb, au-syd, eu-de, ca-tor, jp-tok, jp-osa etc.
This project is released under the Apache 2.0 license. The license's full text can be found in LICENSE.