Skip to content

Commit

Permalink
feat(config): configuration refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ncarlier committed Feb 11, 2020
1 parent 91974bb commit 128a9b4
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 153 deletions.
22 changes: 5 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,11 @@ $ docker run -d --name=readflow ncarlier/readflow

## Configuration

You can configure the server by setting environment variables:

| Variable | Default | Description |
|----------|---------|-------------|
| `APP_LISTEN` | `:8080` | Service listen address |
| `APP_LISTEN_METRICS` | none | Metrics listen address |
| `APP_DB` | `postgres://postgres:testpwd@localhost/reader_test` | Database connection string |
| `APP_AUTHN` | `https://login.nunux.org/auth/realms/readflow` | Authentication method ("mock", "proxy" or OIDC if URL) |
| `APP_BROKER` | none | External event broker URI for outgoing events |
| `APP_PUBLIC_URL` | `https://api.readflow.app` | Public URL |
| `APP_SENTRY_DSN` | none | Sentry DSN URL for error reporting |
| `APP_LOG_LEVEL` | `info` | Logging level (`debug`, `info`, `warn` or `error`) |
| `APP_LOG_PRETTY` | `false` | Plain text log output format if true (JSON otherwise) |
| `APP_LOG_OUTPUT` | `stdout` | Log output target (`stdout` or `file://sample.log`) |

You can also override these settings using program parameters.
Type `readflow --help` to see options.
Readflow can be configured by using command line parameters or by setting environment variables.

Type `readflow -h` to display all parameters and related environment variables.

All configuration variables are described in [etc/default/readflow.env](./etc/default/readflow.env) file.

## UI

Expand Down
39 changes: 39 additions & 0 deletions etc/default/readflow.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
###
# readflow configuration
###

# Authentication method
# - `mock`: Generic user use for testing
# - `proxy`: Use "X-WEBAUTH-USER" or "X-Auth-Username" HTTP header to extract username
# - `https://...`: Use OpenID Connect provider
READFLOW_AUTHN="https://login.nunux.org/auth/realms/readflow"

# External event broker URI for outgoing events, deactivated by default
# Example: "https://example.com/event"
READFLOW_BROKER=

# Database connection string
READFLOW_DB="postgres://postgres:testpwd@localhost/readflow_test?sslmode=disable")

# Image proxy service, deactivated by default
READFLOW_IMAGE_PROXY=

# HTTP listen address
# Examples: "localhost:8080" or ":8080" for all interfaces
READFLOW_LISTEN_ADDR=":8080"

# Metrics listen address (aka: Prometheus metrics endpoint), deactivated by default
# Example: ":9090"
READFLOW_LISTEN_METRICS=

# Log level (debug, info, warn, error), default is "info"
READFLOW_LOG_LEVEL="info"

# Output human readable logs, default is "false"
READFLOW_LOG_PRETTY=flase

# Public URL
READFLOW_PUBLIC_URL="https://api.readflow.app"

# Sentry DSN URL, deactivated by default
READFLOW_SENTRY_DSN=
53 changes: 29 additions & 24 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ import (
"syscall"
"time"

"github.com/ncarlier/readflow/pkg/job"
"github.com/ncarlier/readflow/pkg/metric"

eventbroker "github.com/ncarlier/readflow/pkg/event-broker"
_ "github.com/ncarlier/readflow/pkg/event-listener"
"github.com/ncarlier/readflow/pkg/service"

"github.com/ncarlier/readflow/pkg/db"

"github.com/ncarlier/readflow/pkg/api"
"github.com/ncarlier/readflow/pkg/config"
configflag "github.com/ncarlier/readflow/pkg/config/flag"
"github.com/ncarlier/readflow/pkg/db"
eventbroker "github.com/ncarlier/readflow/pkg/event-broker"
_ "github.com/ncarlier/readflow/pkg/event-listener"
"github.com/ncarlier/readflow/pkg/job"
"github.com/ncarlier/readflow/pkg/logger"
"github.com/ncarlier/readflow/pkg/metric"
"github.com/ncarlier/readflow/pkg/service"
"github.com/ncarlier/readflow/pkg/version"
"github.com/rs/zerolog/log"
)
Expand All @@ -38,28 +36,35 @@ func init() {
}

func main() {
flag.Parse()
// Get global configuration
conf := config.Config{}
configflag.Bind(&conf, "READFLOW")

conf := config.Get()
// Parse command line (and environment variables)
flag.Parse()

if *conf.Version {
// Show version if asked
if *version.ShowVersion {
version.Print()
return
os.Exit(0)
}

// Export configurations vars
config.ExportVars(conf)

// Configure the logger
logger.Configure(*conf.LogLevel, *conf.LogPretty, *conf.SentryDSN)
logger.Configure(conf.LogLevel, conf.LogPretty, conf.SentryDSN)

log.Debug().Msg("starting readflow server...")

// Configure the DB
_db, err := db.Configure(*conf.DB)
_db, err := db.Configure(conf.DB)
if err != nil {
log.Fatal().Err(err).Msg("could not configure database")
}

// Configure Event Broker
_, err = eventbroker.Configure(*conf.Broker)
_, err = eventbroker.Configure(conf.Broker)
if err != nil {
log.Fatal().Err(err).Msg("could not configure event broker")
}
Expand All @@ -74,21 +79,21 @@ func main() {
scheduler := job.StartNewScheduler(_db)

server := &http.Server{
Addr: *conf.ListenAddr,
Handler: api.NewRouter(conf),
Addr: conf.ListenAddr,
Handler: api.NewRouter(&conf),
}

var metricsServer *http.Server
if *conf.ListenMetricsAddr != "" {
if conf.ListenMetricsAddr != "" {
metricsServer = &http.Server{
Addr: *conf.ListenMetricsAddr,
Addr: conf.ListenMetricsAddr,
Handler: metric.NewRouter(),
}
metric.StartCollectors(_db)
go func() {
log.Info().Str("listen", *conf.ListenMetricsAddr).Msg("metrics server started")
log.Info().Str("listen", conf.ListenMetricsAddr).Msg("metrics server started")
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Str("listen", *conf.ListenMetricsAddr).Msg("could not start metrics server")
log.Fatal().Err(err).Str("listen", conf.ListenMetricsAddr).Msg("could not start metrics server")
}
}()
}
Expand Down Expand Up @@ -122,10 +127,10 @@ func main() {

api.Start()

log.Info().Str("listen", *conf.ListenAddr).Msg("server started")
log.Info().Str("listen", conf.ListenAddr).Msg("server started")

if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Str("listen", *conf.ListenAddr).Msg("could not start the server")
log.Fatal().Err(err).Str("listen", conf.ListenAddr).Msg("could not start the server")
}

<-done
Expand Down
25 changes: 12 additions & 13 deletions pkg/api/image-proxy.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

package api

import (
"io"
"net"
"net/http"
"strings"
"net"
"io"

"github.com/ncarlier/readflow/pkg/config"
)
Expand Down Expand Up @@ -60,33 +59,33 @@ func imgProxyHandler(conf *config.Config) http.Handler {
}

// Redirect if image proxy service not configured
if conf.ImageProxy == nil || *conf.ImageProxy == "" {
if conf.ImageProxy == "" {
http.Redirect(w, r, img, 301)
return
}

// Build image proxy client
client := &http.Client{}
req, err := http.NewRequest("GET", *conf.ImageProxy + "/resize?" + q.Encode(), nil)
if err != nil {
req, err := http.NewRequest("GET", conf.ImageProxy+"/resize?"+q.Encode(), nil)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
}

// Manage request headers
req.Header.Set("User-Agent", userAgent)
req.Header.Set("User-Agent", userAgent)
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
appendHostToXForwardHeader(req.Header, clientIP)
}
delHopHeaders(r.Header)

// Do proxy request
resp, err := client.Do(req)
if err != nil {
resp, err := client.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
}
defer resp.Body.Close()

// Create proxy response
delHopHeaders(resp.Header)
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Info struct {
func index(conf *config.Config) http.Handler {
v := Info{
Version: version.Version,
Authority: *config.Get().AuthN,
Authority: conf.AuthN,
VAPID: service.Lookup().GetProperties().VAPIDPublicKey,
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewRouter(conf *config.Config) *http.ServeMux {
return fmt.Sprintf("%d", time.Now().UnixNano())
}

authMiddleware := middleware.Auth(*conf.AuthN)
authMiddleware := middleware.Auth(conf.AuthN)

for _, route := range routes {
var handler http.Handler
Expand Down
78 changes: 10 additions & 68 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,73 +1,15 @@
package config

import (
"flag"
"os"
"strconv"
)

// Config contain global configuration
type Config struct {
ListenAddr *string
ListenMetricsAddr *string
DB *string
Broker *string
AuthN *string
PublicURL *string
Version *bool
LogPretty *bool
LogLevel *string
SentryDSN *string
ImageProxy *string
}

var config = &Config{
ListenAddr: flag.String("listen", getEnv("LISTEN", ":8080"), "Service listen address"),
ListenMetricsAddr: flag.String("listen-metrics", getEnv("LISTEN_METRICS", ""), "Metrics listen address"),
DB: flag.String("db", getEnv("DB", "postgres://postgres:testpwd@localhost/readflow_test?sslmode=disable"), "Database connection string"),
Broker: flag.String("broker", getEnv("BROKER", ""), "External event broker URI for outgoing events"),
AuthN: flag.String("authn", getEnv("AUTHN", "https://login.nunux.org/auth/realms/readflow"), "Authentication method (\"mock\", \"proxy\" or OIDC if URL)"),
PublicURL: flag.String("public-url", getEnv("PUBLIC_URL", "https://api.readflow.app"), "Public URL"),
Version: flag.Bool("version", false, "Print version"),
LogPretty: flag.Bool("log-pretty", getBoolEnv("LOG_PRETTY", false), "Output human readable logs"),
LogLevel: flag.String("log-level", getEnv("LOG_LEVEL", "info"), "Log level (debug, info, warn, error)"),
SentryDSN: flag.String("sentry-dsn", getEnv("SENTRY_DSN", ""), "Sentry DSN URL"),
ImageProxy: flag.String("image-proxy", getEnv("IMAGE_PROXY", ""), "Image proxy service (passthrough if empty)"),
}

func init() {
// set shorthand parameters
const shorthand = " (shorthand)"
usage := flag.Lookup("listen").Usage + shorthand
flag.StringVar(config.ListenAddr, "l", *config.ListenAddr, usage)
usage = flag.Lookup("version").Usage + shorthand
flag.BoolVar(config.Version, "v", *config.Version, usage)
}

// Get global configuration
func Get() *Config {
return config
}

func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv("APP_" + key); ok {
return value
}
return fallback
}

func getIntEnv(key string, fallback int) int {
strValue := getEnv(key, strconv.Itoa(fallback))
if value, err := strconv.Atoi(strValue); err == nil {
return value
}
return fallback
}

func getBoolEnv(key string, fallback bool) bool {
strValue := getEnv(key, strconv.FormatBool(fallback))
if value, err := strconv.ParseBool(strValue); err == nil {
return value
}
return fallback
ListenAddr string `flag:"listen-addr" desc:"HTTP listen address" default:":8080"`
ListenMetricsAddr string `flag:"listen-metrics" desc:"Metrics listen address"`
DB string `flag:"db" desc:"Database connection string" default:"postgres://postgres:testpwd@localhost/readflow_test?sslmode=disable"`
Broker string `flag:"broker" desc:"External event broker URI for outgoing events"`
AuthN string `flag:"authn" desc:"Authentication method (\"mock\", \"proxy\" or OIDC if URL)" default:"https://login.nunux.org/auth/realms/readflow"`
PublicURL string `flag:"public-url" desc:"Public URL" default:"https://api.readflow.app"`
LogPretty bool `flag:"log-pretty" desc:"Output human readable logs" default:"false"`
LogLevel string `flag:"log-level" desc:"Log level (debug, info, warn, error)" default:"info"`
SentryDSN string `flag:"sentry-dsn" desc:"Sentry DSN URL"`
ImageProxy string `flag:"image-proxy" desc:"Image proxy service (passthrough if empty)"`
}
33 changes: 10 additions & 23 deletions pkg/config/expvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,19 @@ package config

import (
"expvar"
"time"
)

var conf = expvar.NewMap("config")
var configMap = expvar.NewMap("config")

func getConfString(val *string) func() interface{} {
return func() interface{} {
if val == nil {
return nil
}
return *val
}
func exportConfigVar(key, value string) {
configMap.Set(key, new(expvar.String))
configMap.Get(key).(*expvar.String).Set(value)
}

func getConfDur(val *time.Duration) func() interface{} {
return func() interface{} {
if val == nil {
return nil
}
return val.String()
}
}

func init() {
conf.Set("addr", expvar.Func(getConfString(config.ListenAddr)))
conf.Set("authn", expvar.Func(getConfString(config.AuthN)))
conf.Set("public-url", expvar.Func(getConfString(config.PublicURL)))
conf.Set("image-proxy", expvar.Func(getConfString(config.ImageProxy)))
// ExportVars export some configuration variables to expvar
func ExportVars(conf Config) {
exportConfigVar("addr", conf.ListenAddr)
exportConfigVar("authn", conf.ListenAddr)
exportConfigVar("public-url", conf.PublicURL)
exportConfigVar("image-proxy", conf.ImageProxy)
}
Loading

0 comments on commit 128a9b4

Please sign in to comment.