Self hosted Decision API

🚧

Early Adoption Phase

This feature is in early adoption, if you want to try it feel free to contact us at [email protected]

Introduction

Although we provide a cloud hosted Decision API that is fast and scalable, you can still choose to host the Decision API on your premises instead of using the cloud one.

The data collection part of the Flagship platform is still hosted on our side, and the Decision API will just batch send the hit tracking calls to our own cloud-hosted data collection on a regular basis.

The Decision API that you can install on-premise is almost the same as the cloud-based Decision API. Because the cloud-based Decision API is multi-tenant, and the on-premise version is single-tenant, there are some added connectors in the cloud-based version such as storage interface for visitor assignments. But the decision business logic is completely identical.

How does it work?

The Decision API is a Go based binary or docker image that synchronizes with the use cases you created on the Flagship platform. The decision logic (context targeting & visitor assignments) is done locally, which makes it really fast.

Here is a high level overview of the On premise Decision API architecture:

1075

Why self-host the Decision API

There are multiple reasons why you would want to host the Decision API yourself, such as:

  • You need a < 30ms response time from the API
  • You have a volume of API calls that goes beyond the cloud based Decision API hard limits
  • You have network restrictions setup that prevent your apps from calling an external service
  • You do not want to use the SDK with the bucketing mode for any reason (like not wanting to add a third-party dependancy to your stack)

Caveats

Visitor assignments caching

Thanks to our hashing algorithm, a unique visitor (with a unique ID) will always see the same variation of a use case, if the traffic allocation of the variations stays the same.
If you're using a high level Flagship feature such as Experience Continuity, 1 visitor 1 experiment or Dynamic Allocation, or if you manually change the traffic allocation of your variations, the hashing algorithm is not enough. You need some sort of assignment cache to store the visitor assignment to the variation.
Flagship cloud-hosted Decision API provides such a caching mechanism for you by default using our own high-velocity key-value store.

If you want to use those features when hosting the DecisionAPI on premise, you need to configure your own cache to store visitor assignments. See the Configuration below to set it up.

Installation

The Flagship Decision API can be installed and deployed in your infrastructure either by downloading and running the binary, or pulling and running the docker image in your orchestration system.

Using a binary

You can download the latest binary here: https://github.com/flagship-io/decision-api/releases

Using a Docker image

You can pull the latest docker image from docker hub:
docker pull flagshipio/decision-api

Running

Using a binary

Download the latest release on github and then simply run:

ENV_ID={your_environment_id} API_KEY={your_api_key} ./decision-api

The server will run on the port 8080 by default. You can override this configuration (see Configuration)

Running with Docker

Run the following command to start the server with Docker

docker run -p 8080:8080 -e ENV_ID={your_env_id} -e API_KEY={your_api_key} flagshipio/decision-api

Configuration

You can configure the Decision API using 2 ways:

  • YAML configuration file
  • Environment Variables

Using a configuration file

Create a config.yaml along your app file, or mount it in docker in location /config.yaml:

docker run -p 8080:8080 -v ./config.yaml:/config.yaml flagshipio/decision-api

The configuration file should look like this:

env_id: "env_id" # Your Flagship Environment ID
api_key: "api_key" # Your Flagship API Key
address: ":8080"  # the listening address of the server
log:
  level: "warn"  # minimal log level to output
  format: "text"  # log format output
polling_internal: "1m"  # the polling interval to synchronize with Flagship platform

# Cache
cache:
  type: local # or 'redis' or 'none' (if you do not want to using visitor cache)
  options:
    dbPath: ./data
    #redisHost: 'localhost:6379' # for redis storage
    #redisUsername: username     # (Optional) for redis storage
    #redisPassword: password     # (Optional) for redis storage
    #redisDb: 0    # (Optional) for redis storage

Using environment variables

You can override each configuration variables from the configuration file using environment variables.
Just name your env variables the same as the config file, but with the following rules:

  • Env variable name should be UPPERCASE
    Example: ENV_ID
  • Sub configuration level are defined using a _ sign
    Example: CACHE_TYPE
    Example: CACHE_OPTIONS_DBPATH

Here is a Docker example using environment variables to setup local caching:

docker run -p 8080:8080 -e ENV_ID={your_env_id} -e API_KEY={your_api_key} -e CACHE_TYPE=local -e CACHE_OPTIONS_DBPATH=./data -v ./config.yaml:/config.yaml flagshipio/decision-api

Here is a Docker Compose example of using Redis as a visitor cache engine:

version: "3"
services:
  decision:
    image: flagshipio/decision-api
    ports:
      - 8080:8080
    environment:
      ENV_ID: "env_id"
      API_KEY: "api_key"
      CACHE_TYPE: redis
      CACHE_OPTIONS_REDISHOST: "redis:6379"
    depends_on:
      - redis

  redis:
    image: redis

That you can run using docker-compose up -d

Configuration parameters

You can use the following parameters to customize the Decision API.
Each parameter is named as in the config.yaml file, and the matching environment variable is parenthesis.

ParameterTypeRequired
env_id (ENV_ID)stringyesThe Flagship environment ID. You can get it from the Flagship platform. Default to empty string
api_key (API_KEY)stringyesThe Flagship API Key for this environment ID. You can get it from the Flagship platform. Default to empty string
address (ADDRESS)stringnoThe server address to listen for requests. Default to ":8080"
cors.enabled (CORS_ENABLED)boolnoIf true, the server will return the cors response headers necessary for cross origins API calls. Default to true
cors.allowed_origins (CORS_ALLOWED_ORIGINS)stringnoIf the cors are enabled, this option will set the Access-Control-Allow-Origin response headers. Default to "*"
log.level (LOG_LEVEL)stringnoSet the minimum log level that will be send to output. Can be trace, debug, info, warn, error, fatal, panic. Default to "warning"
log.format (LOG_FORMAT)stringnoSet the output log format. Can be either "text" or "json". Default to "text"
polling_interval (POLLING_INTERVAL)stringnoThe polling frequency (as parsable by the ParseDuration method) to synchronize with your Flagship configuration. Default to 60s
cache.type (CACHE_TYPE)stringnoIf you want to enable caching for the visitor assignment. Can be "memory", "redis", "dynamo" or "local". Default to empty string.
cache.options.dbPath (CACHE_OPTIONS_DBPATH)stringnoIf you chose local cache type, this is the path of the file where the cache will be stored. Default to empty string
cache.options.redisHost (CACHE_OPTIONS_REDISHOST)stringnoIf you chose redis cache type, this is the host for your redis server
cache.options.redisUsername (CACHE_OPTIONS_REDISUSERNAME)stringnoIf you chose redis cache type, this is the username for your redis server
cache.options.redisUsername (CACHE_OPTIONS_REDISPASSWORD)stringnoIf you chose redis cache type, this is the password for your redis server
cache.options.redisDb (CACHE_OPTIONS_REDISDB)intnoIf you chose redis cache type, this is the db number for your redis server. Default to 0 (default DB)
cache.options.redisTls (CACHE_OPTIONS_REDISTLS)boolnoIf true, redis client will be set to connect using TLS to the redis server. Default to false
cache.options.dynamoTableName (CACHE_OPTIONS_DYNAMOTABLENAME)stringnoThe table name to store cache assignments when using DynamoDB. Default to "visitor-assignments"
cache.options.dynamoPKSeparator (CACHE_OPTIONS_DYNAMOPKSEPARATOR)stringnoThe primary key separator between env ID & visitor ID to store cache assignments when using DynamoDB. Default to "."
cache.options.dynamoPKField (CACHE_OPTIONS_DYNAMOPKFIELD)stringnoThe primary key field name to store cache assignments when using DynamoDB. Default to "id"
cache.options.dynamoGetTimeout (CACHE_OPTIONS_DYNAMOGETTIMEOUT)stringnoThe timeout for getting previously stored visitor cache assignment when using DynamoDB. Default to 1s

Custom visitor cache assignment connector

The Decision API provides 3 cache systems to store and retrieve visitor assignments to allow traffic allocation changes for live use cases:

  • in memory: the assignments are stored in memory
  • local: the assignments are stored in a local file using a key/value database
  • redis: the assignments are stored in a redis server

If you want to use another cache management system, you still can, but you will need to create a Go application that runs the Decision API package, and implement your own visitor assignment interface.

Here is an example of how you can do that:

package main

import (
	"log"
	"net/http"
	"os"
	"time"

	"github.com/flagship-io/decision-api/pkg/connectors"
	"github.com/flagship-io/decision-api/pkg/server"
	common "github.com/flagship-io/flagship-common"
)

type CustomAssignmentManager struct {
}

func (m *CustomAssignmentManager) LoadAssignments(envID string, visitorID string) (*common.VisitorAssignments, error) {
	// TODO implement this method
	return nil, nil
}

func (m *CustomAssignmentManager) ShouldSaveAssignments(context connectors.SaveAssignmentsContext) bool {
	// TODO implement this method
	return true
}

func (m *CustomAssignmentManager) SaveAssignments(envID string, visitorID string, vgIDAssignments map[string]*common.VisitorCache, date time.Time) error {
	// TODO implement this method
	return nil
}

func main() {
	srv, err := server.CreateServer(
		os.Getenv("ENV_ID"),
		os.Getenv("API_KEY"),
		":8080",
		server.WithAssignmentsManager(&CustomAssignmentManager{}),
	)

	if err != nil {
		log.Fatalf("error when creating server: %v", err)
	}

	log.Printf("server listening on :8080")
	if err := srv.Listen(); err != http.ErrServerClosed {
		log.Fatalf("error when starting server: %v", err)
	}
}

API Reference

The self-hosted Decision API has the same relative endpoints than the cloud-base Decision API V2 (latest) . The only difference is that you do not need to put your environment ID inside the URL path (because the environment ID is already configured when starting the Decision API).

So instead of calling
https://decision.flagship.io/v2/{{YOUR_ENVIRONMENT_ID}}/campaigns

You would call
https://your.decision.api.instance/v2/campaigns

You can find the Swagger API doc at the /v2/swagger/index.html URL when running the application.

Metrics endpoint

On top of the usual endpoints, the open source Decision API also exposes a /v2/metrics endpoint, which can be used to monitor the API performance, errors and response time.
The metrics ouput format looks like:

{
  "cmdline": [
    "./bin/server"
  ],
  "handlers.activate.errors": 0,
  "handlers.activate.response_time.p50": 0,
  "handlers.activate.response_time.p90": 0,
  "handlers.activate.response_time.p95": 0,
  "handlers.activate.response_time.p99": 0,
  "handlers.campaign.errors": 0,
  "handlers.campaign.response_time.p50": 0,
  "handlers.campaign.response_time.p90": 0,
  "handlers.campaign.response_time.p95": 0,
  "handlers.campaign.response_time.p99": 0,
  "handlers.campaigns.errors": 0,
  "handlers.campaigns.response_time.p50": 1,
  "handlers.campaigns.response_time.p90": 4,
  "handlers.campaigns.response_time.p95": 10,
  "handlers.campaigns.response_time.p99": 11,
  "handlers.flags.errors": 0,
  "handlers.flags.response_time.p50": 1,
  "handlers.flags.response_time.p90": 1,
  "handlers.flags.response_time.p95": 1,
  "handlers.flags.response_time.p99": 1,
  "memstats": {
		// typical https://pkg.go.dev/runtime#MemStats struct
  }
}