Giter Site home page Giter Site logo

mwitkow / go-flagz Goto Github PK

View Code? Open in Web Editor NEW
203.0 7.0 24.0 398 KB

Dynamic flag management for Go.

License: Apache License 2.0

Go 99.25% Makefile 0.15% Protocol Buffer 0.21% Shell 0.38%
golang configuration etcd kubernetes flags dynamic-flags

go-flagz's Introduction

Go FlagZ

Travis Build Go Report Card GoDoc SourceGraph codecov Apache 2.0 License

Dynamic, thread-safe flag variables that can be modified at runtime through etcd or Kubernetes.

For a similar project for JVM languages (Java, scala) see java-flagz

This sounds crazy. Why?

File-based or command-line configuration can only be changed when a service restarts. Dynamic flags provide flexibility in normal operations and emergencies. Two examples:

  • A new feature launches that you want to A/B test. You want to gradually enable it for a certain fraction of user requests (1%, 5%, 20%, 50%, 100%) without the need to restart servers.
  • Your service is getting overloaded and you want to disable certain costly features. You can't afford restarting because you'd lose important capacity.

All of this can be done simultaneously across a whole shard of your services.

Features

  • compatible with popular flag replacement spf13/pflag (e.g. ones using spf13/cobra)
  • dynamic flag that are thread-safe and efficient:
    • DynInt64
    • DynFloat64
    • DynString
    • DynDuration
    • DynStringSlice
    • DynJSON - a flag that takes an arbitrary JSON struct
    • DynProto3 - a flag that takes a proto3 struct in JSONpb or binary form
  • validator functions for each flag, allows the user to provide checks for newly set values
  • notifier functions allow user code to be subscribed to flag changes
  • Kubernetes ConfigMap watcher, see configmap/README.md.
  • etcd based watcher that syncs values from a distributed Key-Value store into the program's memory
  • Prometheus metric for checksums of the current flag configuration
  • a /debug/flagz HandlerFunc endpoint that allows for easy inspection of the service's runtime configuration

Here's a teaser of the debug endpoint:

Status Endpoint

Examples

Declare a single pflag.FlagSet in some public package (e.g. common.SharedFlagSet) that you'll use throughout your server.

Dynamic JSON flag with a validator and notifier

var (
  limitsConfigFlag = flagz.DynJSON(
    common.SharedFlagSet, 
    "rate_limiting_config", 
    &rateLimitConfig{ DefaultRate: 10, Policy: "allow"},
    "Config for service's rate limit",
  ).WithValidator(rateLimitConfigValidator).WithNotifier(onRateLimitChange)
)

This declares a JSON flag of type rateLimitConfig with a default value. Whenever the config changes (statically or dynamically) the rateLimitConfigValidator will be called. If it returns no errors, the flag will be updated and onRateLimitChange will be called with both old and new, allowing the rate-limit mechanism to re-tune.

Dynamic feature flags

var (
  featuresFlag = flagz.DynStringSlice(common.SharedFlagSet, "enabled_features", []string{"fast_index"}, "list of enabled feature markers")
)
...
func MyHandler(resp http.ResponseWriter, req *http.Request) {
   ...
   if existsInStringSlice("fast_index", featuresFlag.Get()) {
     doFastIndex(req)
   }
   ...
}

All access to featuresFlag, which is a []string flag, is synchronised across go-routines using atomic pointer swaps.

Watching for changes from etcd

// First parse the flags from the command line, as normal.
common.SharedFlagSet.Parse(os.Args[1:])
w, err := watcher.New(common.SharedFlagSet, etcdClient, "/my_service/flagz", logger)
if err != nil {
  logger.Fatalf("failed setting up %v", err)
}
// Read flagz from etcd and update their values in common.SharedFlagSet
if err := w.Initialize(); err != nil {
	log.Fatalf("failed setting up %v", err)
}
// Start listening of dynamic flags from etcd.
w.Start()

The watcher's go-routine will watch for etcd value changes and synchronise them with values in memory. In case a value fails parsing or the user-specified validator, the key in etcd will be atomically rolled back.

More examples:

Status

This code is production quality. It's been running happily in production at Improbable for a few months.

Features planned:

  • - #11 monitoring of FlagSet checksus using a Prometheus handler
  • - #12 support for standard flag (requires changes in spf13/pflag interfaces)

License

go-flagz is released under the Apache 2.0 license. See the LICENSE file for details.

go-flagz's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-flagz's Issues

Idea. Docker integration

Since the flags are dynamic this means that all your docker systems don't need to have configs that tell them about other systems they need to talk to.

If anyone has a decent example of 2 or 3 services being dockerised and using this go-flagz approach it would be very helpful think.

Maybe example the example of this being put into the code base in this repo.

It really shows off how this lib solves a tedious infrastructure problem !!

Implement Prometheus checksum monitoring

Prometheus is a very popular monitoring stack. Unfortunately, it doesn't support strings as values.

Use case:
being able to count the checksums of static and dynamic flags in a pre-defined flagset across many server.

The typical approach in Prometheus (e.g. for build labels) is to export a value 1 with the string inside a label. This allows for easy counting of servers that have that particular value.

The resulting Prometheus metrics would look as follows:

flagz_checksum{set="default",type="dynamic",checksum="9fa85e70"} 1
flagz_checksum{set="default",type="static",checksum="53fd4b02"} 1

These checksums would need to be calculated on-scrape. Prometheus already has support for it with GaugeFunc but it doesn't allow to set labels, and only outputs the value on scrape.

To achieve this, a custom Collector needs to be implemented.

Remove races due to using plain pointers

Normal pflag and normal flag package use plain pointers. This is useful if the assumption is that no values will change past initialization of a server.

However, for dynamic flag updating, it causes races. In practice, everything is alright if running on GOMAXPROC=1, but if you go to multiple threads, stuff can break.

An example of running go test -race . in etcd directory:

michal:~/code/mygo/src/github.com/mwitkow/go-flagz/etcd/ (dynamic_flags*) $ go test -race .                                                     [15:42:47]
2016-03-22 15:42:59.314408 E | etcdserver: cannot monitor file descriptor usage (cannot get FDUsage on darwin)
==================
WARNING: DATA RACE
Read by goroutine 10:
  runtime.convT2E()
      /usr/local/Cellar/go/1.6/libexec/src/runtime/iface.go:128 +0x0
  github.com/mwitkow/go-flagz/etcd_test.(*UpdaterTestSuite).Test_DynamicUpdate.func1()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:99 +0x56
  github.com/mwitkow/go-flagz/etcd_test.eventually()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:168 +0x92
  github.com/mwitkow/go-flagz/etcd_test.(*UpdaterTestSuite).Test_DynamicUpdate()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:100 +0x990
  runtime.call32()
      /usr/local/Cellar/go/1.6/libexec/src/runtime/asm_amd64.s:472 +0x3d
  reflect.Value.Call()
      /usr/local/Cellar/go/1.6/libexec/src/reflect/value.go:303 +0xcd
  github.com/stretchr/testify/suite.Run.func2()
      /Users/michal/code/mygo/src/github.com/stretchr/testify/suite/suite.go:94 +0x276
  testing.tRunner()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:473 +0xdc

Previous write by goroutine 18:
  flag.(*intValue).Set()
      /usr/local/Cellar/go/1.6/libexec/src/flag/flag.go:117 +0x86
  flag.(*FlagSet).Set()
      /usr/local/Cellar/go/1.6/libexec/src/flag/flag.go:363 +0x2ab
  github.com/mwitkow/go-flagz/etcd.(*Updater).watchForUpdates()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater.go:175 +0x10f2

Goroutine 10 (running) created at:
  testing.RunTests()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:582 +0xae2
  github.com/stretchr/testify/suite.Run()
      /Users/michal/code/mygo/src/github.com/stretchr/testify/suite/suite.go:102 +0x7a3
  github.com/mwitkow/go-flagz/etcd_test.TestUpdaterSuite()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:156 +0x595
  testing.tRunner()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:473 +0xdc

Goroutine 18 (running) created at:
  github.com/mwitkow/go-flagz/etcd.(*Updater).Start()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater.go:86 +0x14d
  github.com/mwitkow/go-flagz/etcd_test.(*UpdaterTestSuite).Test_DynamicUpdate()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:94 +0x3f4
  runtime.call32()
      /usr/local/Cellar/go/1.6/libexec/src/runtime/asm_amd64.s:472 +0x3d
  reflect.Value.Call()
      /usr/local/Cellar/go/1.6/libexec/src/reflect/value.go:303 +0xcd
  github.com/stretchr/testify/suite.Run.func2()
      /Users/michal/code/mygo/src/github.com/stretchr/testify/suite/suite.go:94 +0x276
  testing.tRunner()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:473 +0xdc
==================
==================
WARNING: DATA RACE
Read by goroutine 46:
  runtime.convT2E()
      /usr/local/Cellar/go/1.6/libexec/src/runtime/iface.go:128 +0x0
  github.com/mwitkow/go-flagz/etcd_test.(*UpdaterTestSuite).Test_DynamicUpdateRestoresGoodState.func3()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:141 +0x56
  github.com/mwitkow/go-flagz/etcd_test.eventually()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:168 +0x92
  github.com/mwitkow/go-flagz/etcd_test.(*UpdaterTestSuite).Test_DynamicUpdateRestoresGoodState()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:142 +0x11f2
  runtime.call32()
      /usr/local/Cellar/go/1.6/libexec/src/runtime/asm_amd64.s:472 +0x3d
  reflect.Value.Call()
      /usr/local/Cellar/go/1.6/libexec/src/reflect/value.go:303 +0xcd
  github.com/stretchr/testify/suite.Run.func2()
      /Users/michal/code/mygo/src/github.com/stretchr/testify/suite/suite.go:94 +0x276
  testing.tRunner()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:473 +0xdc

Previous write by goroutine 58:
  flag.(*float64Value).Set()
      /usr/local/Cellar/go/1.6/libexec/src/flag/flag.go:206 +0x7f
  flag.(*FlagSet).Set()
      /usr/local/Cellar/go/1.6/libexec/src/flag/flag.go:363 +0x2ab
  github.com/mwitkow/go-flagz/etcd.(*Updater).watchForUpdates()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater.go:175 +0x10f2

Goroutine 46 (running) created at:
  testing.RunTests()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:582 +0xae2
  github.com/stretchr/testify/suite.Run()
      /Users/michal/code/mygo/src/github.com/stretchr/testify/suite/suite.go:102 +0x7a3
  github.com/mwitkow/go-flagz/etcd_test.TestUpdaterSuite()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:156 +0x595
  testing.tRunner()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:473 +0xdc

Goroutine 58 (running) created at:
  github.com/mwitkow/go-flagz/etcd.(*Updater).Start()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater.go:86 +0x14d
  github.com/mwitkow/go-flagz/etcd_test.(*UpdaterTestSuite).Test_DynamicUpdateRestoresGoodState()
      /Users/michal/code/mygo/src/github.com/mwitkow/go-flagz/etcd/updater_test.go:118 +0x4ab
  runtime.call32()
      /usr/local/Cellar/go/1.6/libexec/src/runtime/asm_amd64.s:472 +0x3d
  reflect.Value.Call()
      /usr/local/Cellar/go/1.6/libexec/src/reflect/value.go:303 +0xcd
  github.com/stretchr/testify/suite.Run.func2()
      /Users/michal/code/mygo/src/github.com/stretchr/testify/suite/suite.go:94 +0x276
  testing.tRunner()
      /usr/local/Cellar/go/1.6/libexec/src/testing/testing.go:473 +0xdc
==================
PASS
Found 2 data race(s)
FAIL    github.com/mwitkow/go-flagz/etcd    3.588s

Make `flagz` be usable with normal `flag` and `spf13/pflag`

Currently we're tied to spf13/pflag due to two things.

First, we're using Annotations. It would be trivial to remove them and rely on whether the Value implements Type() and check the prefix for dyn_.

Secondly, and less trivially, the interfaces for pflag.Value and flag.Value don't match, as well as the types of Flag.

https://godoc.org/flag#Flag
https://godoc.org/flag#Value

vs

https://godoc.org/github.com/spf13/pflag#Flag
https://godoc.org/github.com/spf13/pflag#Value

For Dyn* flags, we could achieve complete independence from spf13/pflag vs flag if FlagSet.Var accepted a Value that could be abstracted as common.

However, for monitoring and debug endpoints, we need calls to VisitAll. These return an explicit Flag type that differs between the two.

The second problem can be worked around by just recompiling flagz with s/flag "github.com/spf13/flag"/"flag" ;)

maintenance, fork/copies

hi @mwitkow sorry to bug you.

I was curious if I could get your permission to take some/most of the go-flagz code and integrate it into a slightly different format (for instance using std go flags pattern and naming, a different logging/error handling etc...) probably as a new package in fortio (https://github.com/fortio/fortio) obviously I would preserve your name in the credits and copyright header but curious to get your blessing or hear objections? (do you plan on maintaining go-flagz further? is improbable-io going to?)

ps: if you want we can talk via email or twitter or linked in if that feels better

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.