1package main
2
3import (
4	"crypto/sha256"
5	"flag"
6	"fmt"
7	"io/ioutil"
8	"math/rand"
9	"os"
10	"runtime"
11	"sort"
12	"strings"
13	"time"
14
15	"github.com/go-kit/log/level"
16	"github.com/grafana/dskit/flagext"
17	"github.com/pkg/errors"
18	"github.com/prometheus/client_golang/prometheus"
19	"github.com/prometheus/common/version"
20	"github.com/weaveworks/common/tracing"
21	"gopkg.in/yaml.v2"
22
23	"github.com/cortexproject/cortex/pkg/cortex"
24	"github.com/cortexproject/cortex/pkg/util"
25	util_log "github.com/cortexproject/cortex/pkg/util/log"
26)
27
28// Version is set via build flag -ldflags -X main.Version
29var (
30	Version  string
31	Branch   string
32	Revision string
33)
34
35// configHash exposes information about the loaded config
36var configHash *prometheus.GaugeVec = prometheus.NewGaugeVec(
37	prometheus.GaugeOpts{
38		Name: "cortex_config_hash",
39		Help: "Hash of the currently active config file.",
40	},
41	[]string{"sha256"},
42)
43
44func init() {
45	version.Version = Version
46	version.Branch = Branch
47	version.Revision = Revision
48	prometheus.MustRegister(version.NewCollector("cortex"))
49	prometheus.MustRegister(configHash)
50}
51
52const (
53	configFileOption = "config.file"
54	configExpandENV  = "config.expand-env"
55)
56
57var testMode = false
58
59func main() {
60	var (
61		cfg                  cortex.Config
62		eventSampleRate      int
63		ballastBytes         int
64		mutexProfileFraction int
65		blockProfileRate     int
66		printVersion         bool
67		printModules         bool
68	)
69
70	configFile, expandENV := parseConfigFileParameter(os.Args[1:])
71
72	// This sets default values from flags to the config.
73	// It needs to be called before parsing the config file!
74	flagext.RegisterFlags(&cfg)
75
76	if configFile != "" {
77		if err := LoadConfig(configFile, expandENV, &cfg); err != nil {
78			fmt.Fprintf(os.Stderr, "error loading config from %s: %v\n", configFile, err)
79			if testMode {
80				return
81			}
82			os.Exit(1)
83		}
84	}
85
86	// Ignore -config.file and -config.expand-env here, since it was already parsed, but it's still present on command line.
87	flagext.IgnoredFlag(flag.CommandLine, configFileOption, "Configuration file to load.")
88	_ = flag.CommandLine.Bool(configExpandENV, false, "Expands ${var} or $var in config according to the values of the environment variables.")
89
90	flag.IntVar(&eventSampleRate, "event.sample-rate", 0, "How often to sample observability events (0 = never).")
91	flag.IntVar(&ballastBytes, "mem-ballast-size-bytes", 0, "Size of memory ballast to allocate.")
92	flag.IntVar(&mutexProfileFraction, "debug.mutex-profile-fraction", 0, "Fraction of mutex contention events that are reported in the mutex profile. On average 1/rate events are reported. 0 to disable.")
93	flag.IntVar(&blockProfileRate, "debug.block-profile-rate", 0, "Fraction of goroutine blocking events that are reported in the blocking profile. 1 to include every blocking event in the profile, 0 to disable.")
94	flag.BoolVar(&printVersion, "version", false, "Print Cortex version and exit.")
95	flag.BoolVar(&printModules, "modules", false, "List available values that can be used as target.")
96
97	usage := flag.CommandLine.Usage
98	flag.CommandLine.Usage = func() { /* don't do anything by default, we will print usage ourselves, but only when requested. */ }
99	flag.CommandLine.Init(flag.CommandLine.Name(), flag.ContinueOnError)
100
101	err := flag.CommandLine.Parse(os.Args[1:])
102	if err == flag.ErrHelp {
103		// Print available parameters to stdout, so that users can grep/less it easily.
104		flag.CommandLine.SetOutput(os.Stdout)
105		usage()
106		if !testMode {
107			os.Exit(2)
108		}
109	} else if err != nil {
110		fmt.Fprintln(flag.CommandLine.Output(), "Run with -help to get list of available parameters")
111		if !testMode {
112			os.Exit(2)
113		}
114	}
115
116	if printVersion {
117		fmt.Fprintln(os.Stdout, version.Print("Cortex"))
118		return
119	}
120
121	// Validate the config once both the config file has been loaded
122	// and CLI flags parsed.
123	err = cfg.Validate(util_log.Logger)
124	if err != nil {
125		fmt.Fprintf(os.Stderr, "error validating config: %v\n", err)
126		if !testMode {
127			os.Exit(1)
128		}
129	}
130
131	// Continue on if -modules flag is given. Code handling the
132	// -modules flag will not start cortex.
133	if testMode && !printModules {
134		DumpYaml(&cfg)
135		return
136	}
137
138	if mutexProfileFraction > 0 {
139		runtime.SetMutexProfileFraction(mutexProfileFraction)
140	}
141	if blockProfileRate > 0 {
142		runtime.SetBlockProfileRate(blockProfileRate)
143	}
144
145	util_log.InitLogger(&cfg.Server)
146
147	// Allocate a block of memory to alter GC behaviour. See https://github.com/golang/go/issues/23044
148	ballast := make([]byte, ballastBytes)
149
150	util.InitEvents(eventSampleRate)
151
152	// In testing mode skip JAEGER setup to avoid panic due to
153	// "duplicate metrics collector registration attempted"
154	if !testMode {
155		name := "cortex"
156		if len(cfg.Target) == 1 {
157			name += "-" + cfg.Target[0]
158		}
159
160		// Setting the environment variable JAEGER_AGENT_HOST enables tracing.
161		if trace, err := tracing.NewFromEnv(name); err != nil {
162			level.Error(util_log.Logger).Log("msg", "Failed to setup tracing", "err", err.Error())
163		} else {
164			defer trace.Close()
165		}
166	}
167
168	// Initialise seed for randomness usage.
169	rand.Seed(time.Now().UnixNano())
170
171	t, err := cortex.New(cfg)
172	util_log.CheckFatal("initializing cortex", err)
173
174	if printModules {
175		allDeps := t.ModuleManager.DependenciesForModule(cortex.All)
176
177		for _, m := range t.ModuleManager.UserVisibleModuleNames() {
178			ix := sort.SearchStrings(allDeps, m)
179			included := ix < len(allDeps) && allDeps[ix] == m
180
181			if included {
182				fmt.Fprintln(os.Stdout, m, "*")
183			} else {
184				fmt.Fprintln(os.Stdout, m)
185			}
186		}
187
188		fmt.Fprintln(os.Stdout)
189		fmt.Fprintln(os.Stdout, "Modules marked with * are included in target All.")
190		return
191	}
192
193	level.Info(util_log.Logger).Log("msg", "Starting Cortex", "version", version.Info())
194
195	err = t.Run()
196
197	runtime.KeepAlive(ballast)
198	util_log.CheckFatal("running cortex", err)
199}
200
201// Parse -config.file and -config.expand-env option via separate flag set, to avoid polluting default one and calling flag.Parse on it twice.
202func parseConfigFileParameter(args []string) (configFile string, expandEnv bool) {
203	// ignore errors and any output here. Any flag errors will be reported by main flag.Parse() call.
204	fs := flag.NewFlagSet("", flag.ContinueOnError)
205	fs.SetOutput(ioutil.Discard)
206
207	// usage not used in these functions.
208	fs.StringVar(&configFile, configFileOption, "", "")
209	fs.BoolVar(&expandEnv, configExpandENV, false, "")
210
211	// Try to find -config.file and -config.expand-env option in the flags. As Parsing stops on the first error, eg. unknown flag, we simply
212	// try remaining parameters until we find config flag, or there are no params left.
213	// (ContinueOnError just means that flag.Parse doesn't call panic or os.Exit, but it returns error, which we ignore)
214	for len(args) > 0 {
215		_ = fs.Parse(args)
216		args = args[1:]
217	}
218
219	return
220}
221
222// LoadConfig read YAML-formatted config from filename into cfg.
223func LoadConfig(filename string, expandENV bool, cfg *cortex.Config) error {
224	buf, err := ioutil.ReadFile(filename)
225	if err != nil {
226		return errors.Wrap(err, "Error reading config file")
227	}
228
229	// create a sha256 hash of the config before expansion and expose it via
230	// the config_info metric
231	hash := sha256.Sum256(buf)
232	configHash.Reset()
233	configHash.WithLabelValues(fmt.Sprintf("%x", hash)).Set(1)
234
235	if expandENV {
236		buf = expandEnv(buf)
237	}
238
239	err = yaml.UnmarshalStrict(buf, cfg)
240	if err != nil {
241		return errors.Wrap(err, "Error parsing config file")
242	}
243
244	return nil
245}
246
247func DumpYaml(cfg *cortex.Config) {
248	out, err := yaml.Marshal(cfg)
249	if err != nil {
250		fmt.Fprintln(os.Stderr, err)
251	} else {
252		fmt.Printf("%s\n", out)
253	}
254}
255
256// expandEnv replaces ${var} or $var in config according to the values of the current environment variables.
257// The replacement is case-sensitive. References to undefined variables are replaced by the empty string.
258// A default value can be given by using the form ${var:default value}.
259func expandEnv(config []byte) []byte {
260	return []byte(os.Expand(string(config), func(key string) string {
261		keyAndDefault := strings.SplitN(key, ":", 2)
262		key = keyAndDefault[0]
263
264		v := os.Getenv(key)
265		if v == "" && len(keyAndDefault) == 2 {
266			v = keyAndDefault[1] // Set value to the default.
267		}
268		return v
269	}))
270}
271