Commit cf080930 authored by Mo Firouz's avatar Mo Firouz
Browse files

Streamline commandline arguments and config file options. Merged #68

Rename ops to dashboard.
parent 93c360a7
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ The format is based on [keep a changelog](http://keepachangelog.com/) and this p

### Changed
- Run Facebook friends import after registration completes.
- Streamline command line flags to be inline with the config file. 

### Changed
- Restructure and stabilize API messages.
+1 −1
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ $> nakama
{"level":"info","ts":"$$timestamp$$","msg":"Data directory","path":"$$datadir$$"}
{"level":"info","ts":"$$timestamp$$","msg":"Database connections","dsns":["root@localhost:26257"]}
{"level":"info","ts":"$$timestamp$$","msg":"Evaluating modules","count":0,"modules":[]}
{"level":"info","ts":"$$timestamp$$","msg":"Ops","port":7351}
{"level":"info","ts":"$$timestamp$$","msg":"Dashboard","port":7351}
{"level":"info","ts":"$$timestamp$$","msg":"Dashboard","url":"http://127.0.0.1:7351"}
{"level":"info","ts":"$$timestamp$$","msg":"Client","port":7350}
```
+8 −78
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@ package main

import (
	"database/sql"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
@@ -34,9 +33,8 @@ import (
	"nakama/pkg/social"

	"github.com/armon/go-metrics"
	"github.com/go-yaml/yaml"
	_ "github.com/lib/pq"
	uuid "github.com/satori/go.uuid"
	"github.com/satori/go.uuid"
	"go.uber.org/zap"
)

@@ -54,8 +52,7 @@ func main() {
	semver := fmt.Sprintf("%s+%s", version, commitID)
	http.DefaultClient.Timeout = 1500 * time.Millisecond // Always set default timeout on HTTP client

	consoleLogger := server.NewJSONLogger(os.Stdout) // or NewConsoleLogger

	cmdLogger := server.NewJSONLogger(os.Stdout, true) // or NewConsoleLogger
	if len(os.Args) > 1 {
		switch os.Args[1] {
		case "--version":
@@ -64,25 +61,19 @@ func main() {
		case "doctor":
			cmd.DoctorParse(os.Args[2:])
		case "migrate":
			cmd.MigrateParse(os.Args[2:], consoleLogger)
			cmd.MigrateParse(os.Args[2:], cmdLogger)
		case "admin":
			cmd.AdminParse(os.Args[2:], consoleLogger)
			cmd.AdminParse(os.Args[2:], cmdLogger)
		}
	}

	config := parseArgs(consoleLogger)
	config := server.ParseArgs(cmdLogger, os.Args)
	jsonLogger, multiLogger := server.SetupLogging(config)

	memoryMetricSink := metrics.NewInmemSink(10*time.Second, time.Minute)
	metric := &metrics.FanoutSink{memoryMetricSink}
	metrics.NewGlobal(&metrics.Config{EnableRuntimeMetrics: true, ProfileInterval: 5 * time.Second}, metric)

	jsonLogger := server.NewLogger(consoleLogger, config)
	multiLogger := consoleLogger
	if !server.StdoutLogging {
		// if we aren't printing only to stdout, then we want to multiplex entries
		multiLogger = server.NewMultiLogger(consoleLogger, jsonLogger)
	}

	// Print startup information
	multiLogger.Info("Nakama starting")
	multiLogger.Info("Node", zap.String("name", config.GetName()), zap.String("version", semver))
@@ -110,7 +101,7 @@ func main() {
	socialClient := social.NewClient(5 * time.Second)
	pipeline := server.NewPipeline(config, db, trackerService, matchmakerService, messageRouter, sessionRegistry, socialClient, runtime)
	authService := server.NewAuthenticationService(jsonLogger, config, db, statsService, sessionRegistry, socialClient, pipeline, runtime)
	opsService := server.NewOpsService(jsonLogger, multiLogger, semver, config, statsService)
	dashboardService := server.NewDashboardService(jsonLogger, multiLogger, semver, config, statsService)

	gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1
	cookie := newOrLoadCookie(config.GetDataDir())
@@ -128,7 +119,7 @@ func main() {
		multiLogger.Info("Shutting down")

		authService.Stop()
		opsService.Stop()
		dashboardService.Stop()
		trackerService.Stop()
		runtime.Stop()

@@ -145,67 +136,6 @@ func main() {
	select {}
}

func parseArgs(consoleLogger *zap.Logger) server.Config {
	config := server.NewConfig()

	flags := flag.NewFlagSet("main", flag.ExitOnError)
	flags.BoolVar(&server.VerboseLogging, "verbose", false, "Turn verbose logging on.")
	flags.BoolVar(&server.StdoutLogging, "logtostdout", false, "Log to stdout instead of file.")
	var configPath string
	flags.StringVar(&configPath, "config", "", "The absolute file path to configuration YAML file.")
	var name string
	flags.StringVar(&name, "name", "", "The virtual name of this server.")
	var datadir string
	flags.StringVar(&datadir, "data-dir", "", "The data directory to store server logs.")
	var dsn string
	flags.StringVar(&dsn, "db", "", "The database connection DSN. (default root@127.0.0.1:26257)")
	var port int
	flags.IntVar(&port, "port", -1, "Set port for client connections; all other ports will also be set sequentially.")
	var opsPort int
	flags.IntVar(&opsPort, "ops-port", -1, "Set port for ops dashboard.")

	if err := flags.Parse(os.Args[1:]); err != nil {
		consoleLogger.Error("Could not parse command line arguments - ignoring command-line overrides", zap.Error(err))
	} else {

		if len(configPath) > 0 {
			data, err := ioutil.ReadFile(configPath)
			if err != nil {
				consoleLogger.Error("Could not read config file, using defaults", zap.Error(err))
			} else {
				err = yaml.Unmarshal(data, config)
				if err != nil {
					consoleLogger.Error("Could not parse config file, using defaults", zap.Error(err))
				}
			}
		}

		if len(name) > 0 {
			config.Name = name
		}
		if len(datadir) > 0 {
			config.Datadir = datadir
		}
		if len(dsn) > 0 {
			config.Dsns = []string{dsn}
		}
		if port != -1 {
			config.Port = port
			config.OpsPort = port + 1
		}
		if opsPort != -1 {
			config.OpsPort = opsPort
		}
	}

	// if the runtime path is not overridden, set it to `datadir/modules`
	if config.GetRuntime().Path == "" {
		config.GetRuntime().Path = filepath.Join(config.GetDataDir(), "modules")
	}

	return config
}

func dbConnect(multiLogger *zap.Logger, dsns []string) *sql.DB {
	// TODO config database pooling
	rawurl := fmt.Sprintf("postgresql://%s?sslmode=disable", dsns[0])

pkg/flags/flags.go

0 → 100644
+494 −0
Original line number Diff line number Diff line
// Copyright (c) 2016 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// Package flags provides an interface for automatically creating command line
// options from a struct.
//
// Typically, if one wants to load from a yaml, one has to define a proper
// struct, then yaml.Unmarshal(), this is all good. However, there are
// situations where we want to load most of the configs from the file but
// overriding some configs.
//
// Let's say we use a yaml to config our Db connections and upon start of the
// application we load from the yaml file to get the necessary parameters to
// create the connection. Our base.yaml looks like this
//
//   base.yaml
//   ---
//   mysql:
//     user: 'foo'
//     password: 'xxxxxx'
//     mysql_defaults_file: ./mysql_defaults.ini
//     mysql_socket_path: /var/run/mysqld/mysqld.sock
//     ... more config options ...
//
// we want to load all the configs from it but we want to provide some
// flexibility for the program to connect via a different db user. We could
// define a --user command flag then after loading the yaml file, we override
// the user field with what we get from --user flag.
//
// If there are many overriding like this, manual define these flags is
// tedious. This package provides an automatic way to define this override,
// which is, given a struct, it'll create all the flags which are name using
// the field names of the struct. If one of these flags are set via command
// line, the struct will be modified in-place to reflect the value from command
// line, therefore the values of the fields in the struct are overridden
//
// YAML is just used as an example here. In practice, one can use any struct
// to define flags.
//
// Let's say we have our configration object as the following.
//
//   type logging struct {
//   	 Interval int
//   	 Path     string
//   }
//
//   type socket struct {
//   	 ReadTimeout  time.Duration
//   	 WriteTimeout time.Duration
//   }
//
//   type tcp struct {
//   	 ReadTimeout time.Duration
//   	 socket
//   }
//
//   type network struct {
//   	 ReadTimeout  time.Duration
//   	 WriteTimeout time.Duration
//   	 tcp
//   }
//
//   type Cfg struct {
//   	 logging
//   	 network
//   }
//
// The following code
//
//   func main() {
//     c := &Cfg{}
//     flags.ParseArgs(c, os.Args[1:])
//   }
//
// will create the following flags
//
//   -logging.interval int
//         logging.interval
//   -logging.path string
//         logging.path
//   -network.readtimeout duration
//         network.readtimeout
//   -network.tcp.readtimeout duration
//         network.tcp.readtimeout
//   -network.tcp.socket.readtimeout duration
//         network.tcp.socket.readtimeout
//   -network.tcp.socket.writetimeout duration
//         network.tcp.socket.writetimeout
//   -network.writetimeout duration
//         network.writetimeout
//
// flags to subcommands are naturally suported.
//
//   func main() {
//     cmd := os.Args[1]
//     switch cmd {
//       case "new"
//       c1 := &Cfg1{}
//       ParseArgs(c1, os.Args[2:])
//     case "update":
//       c2 := &Cfg2{}
//       ParseArgs(c2, os.Args[2:])
//
//     ... more sub commands ...
//     }
//   }
//
// One can set Flatten to true when calling NewFlagMakerAdv, in which case,
// flags are created without namespacing. For example,
//
//   type auth struct {
//    Token string
//    Tag   float64
//   }
//
//   type credentials struct {
//    User     string
//    Password string
//    auth
//   }
//
//   type database struct {
//    DBName    string
//    TableName string
//    credentials
//   }
//
//   type Cfg struct {
//    logging
//    database
//   }
//
//   func main() {
//    c := &Cfg{}
//    flags.ParseArgs(c, os.Args[1:])
//   }
//
// will create the following flags
//   -dbname string
//         dbname
//   -interval int
//         interval
//   -password string
//         password
//   -path string
//         path
//   -tablename string
//         tablename
//   -tag float
//         tag
//   -token string
//         token
//   -user string
//         user
//
// Please be aware that usual GoLang flag creation rules apply, i.e., if there are
// duplication in flag names (in the flattened case it's more likely to happen
// unless the caller make due dilligence to create the struct properly), it panics.
//
//
// Note that not all types can have command line flags created for. map, channel
// and function type will not defien a flag corresponding to the field. Pointer
// types are properly handled and slice type will create multi-value command
// line flags. That is, e.g. if a field foo's type is []int, one can use
// --foo 10 --foo 15 --foo 20 to override this field value to be
// []int{10, 15, 20}. For now, only []int, []string and []float64 are supported
// in this fashion.
package flags

import (
	"flag"
	"fmt"
	"reflect"
	"strings"
	"time"
)

// FlagMakingOptions control the way FlagMaker's behavior when defining flags.
type FlagMakingOptions struct {
	// Use lower case flag names rather than the field name/tag name directly.
	UseLowerCase bool
	// Create flags in namespaced fashion
	Flatten bool
	// If there is a struct tag named 'TagName', use its value as the flag name.
	// The purpose is that, for yaml/json parsing we often have something like
	// Foobar string `yaml:"host_name"`, in which case the flag will be named
	// 'host_name' rather than 'foobar'.
	TagName string
	// If there is a struct tag named 'TagUsage', use its value as the usage description.
	TagUsage string
}

// FlagMaker enumerate all the exported fields of a struct recursively
// and create corresponding command line flags. For anonymous fields,
// they are only enumerated if they are pointers to structs.
// Usual GoLang flag rules apply, e.g. duplicated flag names leads to
// panic.
type FlagMaker struct {
	opts *FlagMakingOptions
	// We don't consume os.Args directly unless told to.
	fs *flag.FlagSet
}

// NewFlagMaker creates a default FlagMaker which creates namespaced flags
func NewFlagMaker() *FlagMaker {
	return NewFlagMakerAdv(&FlagMakingOptions{
		UseLowerCase: true,
		Flatten:      false,
		TagName:      "yaml",
		TagUsage:     "usage"})
}

// NewFlagMakerAdv gives full control to create flags.
func NewFlagMakerAdv(options *FlagMakingOptions) *FlagMaker {
	return &FlagMaker{
		opts: options,
		fs:   flag.NewFlagSet("xFlags", flag.ContinueOnError),
	}
}

// NewFlagMakerFlagSet gives full control to create flags.
func NewFlagMakerFlagSet(options *FlagMakingOptions, fs *flag.FlagSet) *FlagMaker {
	return &FlagMaker{
		opts: options,
		fs:   fs,
	}
}

// ParseArgs parses the string arguments which should not contain the program name.
//
// obj is the struct to populate. args are the command line arguments,
// typically obtained from os.Args.
func ParseArgs(obj interface{}, args []string) ([]string, error) {
	fm := NewFlagMaker()
	return fm.ParseArgs(obj, args)
}

// PrintDefaults prints the default value and type of defined flags.
// It just calls the standard 'flag' package's PrintDefaults.
func (fm *FlagMaker) PrintDefaults() {
	fm.fs.PrintDefaults()
}

// ParseArgs parses the arguments based on the FlagMaker's setting.
func (fm *FlagMaker) ParseArgs(obj interface{}, args []string) ([]string, error) {
	v := reflect.ValueOf(obj)
	if v.Kind() != reflect.Ptr {
		return args, fmt.Errorf("top level object must be a pointer. %v is passed", v.Type())
	}
	if v.IsNil() {
		return args, fmt.Errorf("top level object cannot be nil")
	}

	switch e := v.Elem(); e.Kind() {
	case reflect.Struct:
		fm.enumerateAndCreate("", e, "")
	case reflect.Interface:
		if e.Elem().Kind() == reflect.Ptr {
			fm.enumerateAndCreate("", e, "")
		} else {
			return args, fmt.Errorf("interface must have pointer underlying type. %v is passed", v.Type())
		}
	default:
		return args, fmt.Errorf("object must be a pointer to struct or interface. %v is passed", v.Type())
	}

	err := fm.fs.Parse(args)
	return fm.fs.Args(), err
}

func (fm *FlagMaker) enumerateAndCreate(prefix string, value reflect.Value, usage string) {
	switch value.Kind() {
	case
		// do no create flag for these types
		reflect.Map,
		reflect.Uintptr,
		reflect.UnsafePointer,
		reflect.Array,
		reflect.Chan,
		reflect.Func:
		return
	case reflect.Slice:
		// only support slice of strings, ints and float64s
		switch value.Type().Elem().Kind() {
		case reflect.String:
			fm.defineStringSlice(prefix, value, usage)
		case reflect.Int:
			fm.defineIntSlice(prefix, value, usage)
		case reflect.Float64:
			fm.defineFloat64Slice(prefix, value, usage)
		}
		return
	case
		// Basic value types
		reflect.String,
		reflect.Bool,
		reflect.Float32, reflect.Float64,
		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		fm.defineFlag(prefix, value, usage)
		return
	case reflect.Interface:
		if !value.IsNil() {
			fm.enumerateAndCreate(prefix, value.Elem(), usage)
		}
		return
	case reflect.Ptr:
		if value.IsNil() {
			value.Set(reflect.New(value.Type().Elem()))
		}
		fm.enumerateAndCreate(prefix, value.Elem(), usage)
		return
	case reflect.Struct:
		// keep going
	default:
		panic(fmt.Sprintf("unknown reflected kind %v", value.Kind()))
	}

	numFields := value.NumField()
	tt := value.Type()

	for i := 0; i < numFields; i++ {
		stField := tt.Field(i)
		// Skip unexported fields, as only exported fields can be set. This is similar to how json and yaml work.
		if stField.PkgPath != "" && !stField.Anonymous {
			continue
		}
		if stField.Anonymous && fm.getUnderlyingType(stField.Type).Kind() != reflect.Struct {
			continue
		}
		field := value.Field(i)
		optName := fm.getName(stField)
		if len(prefix) > 0 && !fm.opts.Flatten {
			optName = prefix + "." + optName
		}

		usageDesc := fm.getUsage(optName, stField)
		if len(usageDesc) == 0 {
			optName = optName
		}

		fm.enumerateAndCreate(optName, field, usageDesc)
	}
}

func (fm *FlagMaker) getName(field reflect.StructField) string {
	name := field.Tag.Get(fm.opts.TagName)
	if len(name) == 0 {
		if field.Anonymous {
			name = fm.getUnderlyingType(field.Type).Name()
		} else {
			name = field.Name
		}
	}
	if fm.opts.UseLowerCase {
		return strings.ToLower(name)
	}
	return name
}

func (fm *FlagMaker) getUsage(name string, field reflect.StructField) string {
	usage := field.Tag.Get(fm.opts.TagUsage)
	if len(usage) == 0 {
		usage = name
	}
	return usage
}

func (fm *FlagMaker) getUnderlyingType(ttype reflect.Type) reflect.Type {
	// this only deals with *T unnamed type, other unnamed types, e.g. []int, struct{}
	// will return empty string.
	if ttype.Kind() == reflect.Ptr {
		return fm.getUnderlyingType(ttype.Elem())
	}
	return ttype
}

// Each object has its type (which prescribes the possible operations/methods
// could be invoked; it also has an underlying 'kind', int, float, struct etc.
// Since user can freely define types, one 'kind' of object may correpond to
// many types. We cannot do type assertion because types of same kind are still
// different types. Instead, we convert to the primitive types that corresponds
// to the kinds and create flag vars. One thing to know is that, the whole point
// of defineFlag() method is to define flag.Vars that points to certain field
// of the struct so that command line values can modify the struct. We cannot
// define a flag var pointing to arbitrary 'free' varible.

// I wish GoLang had macro...
var (
	stringPtrType  = reflect.TypeOf((*string)(nil))
	boolPtrType    = reflect.TypeOf((*bool)(nil))
	float32PtrType = reflect.TypeOf((*float32)(nil))
	float64PtrType = reflect.TypeOf((*float64)(nil))
	intPtrType     = reflect.TypeOf((*int)(nil))
	int8PtrType    = reflect.TypeOf((*int8)(nil))
	int16PtrType   = reflect.TypeOf((*int16)(nil))
	int32PtrType   = reflect.TypeOf((*int32)(nil))
	int64PtrType   = reflect.TypeOf((*int64)(nil))
	uintPtrType    = reflect.TypeOf((*uint)(nil))
	uint8PtrType   = reflect.TypeOf((*uint8)(nil))
	uint16PtrType  = reflect.TypeOf((*uint16)(nil))
	uint32PtrType  = reflect.TypeOf((*uint32)(nil))
	uint64PtrType  = reflect.TypeOf((*uint64)(nil))
)

func (fm *FlagMaker) defineFlag(name string, value reflect.Value, usage string) {
	// v must be scalar, otherwise panic
	ptrValue := value.Addr()
	switch value.Kind() {
	case reflect.String:
		v := ptrValue.Convert(stringPtrType).Interface().(*string)
		fm.fs.StringVar(v, name, value.String(), usage)
	case reflect.Bool:
		v := ptrValue.Convert(boolPtrType).Interface().(*bool)
		fm.fs.BoolVar(v, name, value.Bool(), usage)
	case reflect.Int:
		v := ptrValue.Convert(intPtrType).Interface().(*int)
		fm.fs.IntVar(v, name, int(value.Int()), usage)
	case reflect.Int8:
		v := ptrValue.Convert(int8PtrType).Interface().(*int8)
		fm.fs.Var(newInt8Value(v), name, usage)
	case reflect.Int16:
		v := ptrValue.Convert(int16PtrType).Interface().(*int16)
		fm.fs.Var(newInt16Value(v), name, usage)
	case reflect.Int32:
		v := ptrValue.Convert(int32PtrType).Interface().(*int32)
		fm.fs.Var(newInt32Value(v), name, usage)
	case reflect.Int64:
		switch v := ptrValue.Interface().(type) {
		case *int64:
			fm.fs.Int64Var(v, name, value.Int(), usage)
		case *time.Duration:
			fm.fs.DurationVar(v, name, value.Interface().(time.Duration), usage)
		default:
			// (TODO) if one type defines time.Duration, we'll create a int64 flag for it.
			// Find some acceptible way to deal with it.
			vv := ptrValue.Convert(int64PtrType).Interface().(*int64)
			fm.fs.Int64Var(vv, name, value.Int(), usage)
		}
	case reflect.Float32:
		v := ptrValue.Convert(float32PtrType).Interface().(*float32)
		fm.fs.Var(newFloat32Value(v), name, usage)
	case reflect.Float64:
		v := ptrValue.Convert(float64PtrType).Interface().(*float64)
		fm.fs.Float64Var(v, name, value.Float(), usage)
	case reflect.Uint:
		v := ptrValue.Convert(uintPtrType).Interface().(*uint)
		fm.fs.UintVar(v, name, uint(value.Uint()), usage)
	case reflect.Uint8:
		v := ptrValue.Convert(uint8PtrType).Interface().(*uint8)
		fm.fs.Var(newUint8Value(v), name, usage)
	case reflect.Uint16:
		v := ptrValue.Convert(uint16PtrType).Interface().(*uint16)
		fm.fs.Var(newUint16Value(v), name, usage)
	case reflect.Uint32:
		v := ptrValue.Convert(uint32PtrType).Interface().(*uint32)
		fm.fs.Var(newUint32Value(v), name, usage)
	case reflect.Uint64:
		v := ptrValue.Convert(uint64PtrType).Interface().(*uint64)
		fm.fs.Uint64Var(v, name, value.Uint(), usage)
	}
}

func (fm *FlagMaker) defineStringSlice(name string, value reflect.Value, usage string) {
	ptrValue := value.Interface().([]string)
	fm.fs.Var(newStringSlice(ptrValue), name, usage)
}

func (fm *FlagMaker) defineIntSlice(name string, value reflect.Value, usage string) {
	ptrValue := value.Interface().([]int)
	fm.fs.Var(newIntSlice(ptrValue), name, usage)
}

func (fm *FlagMaker) defineFloat64Slice(name string, value reflect.Value, usage string) {
	ptrValue := value.Interface().([]float64)
	fm.fs.Var(newFloat64Slice(ptrValue), name, usage)
}

pkg/flags/vars.go

0 → 100644
+244 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading