setting.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. // Copyright 2014 Unknwon
  2. // Copyright 2014 Torkel Ödegaard
  3. package setting
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "net/url"
  9. "os"
  10. "path"
  11. "path/filepath"
  12. "regexp"
  13. "runtime"
  14. "strings"
  15. "github.com/go-macaron/session"
  16. "gopkg.in/ini.v1"
  17. "github.com/grafana/grafana/pkg/log"
  18. "github.com/grafana/grafana/pkg/util"
  19. )
  20. type Scheme string
  21. const (
  22. HTTP Scheme = "http"
  23. HTTPS Scheme = "https"
  24. )
  25. const (
  26. DEV string = "development"
  27. PROD string = "production"
  28. TEST string = "test"
  29. )
  30. var (
  31. // App settings.
  32. Env string = DEV
  33. AppUrl string
  34. AppSubUrl string
  35. InstanceName string
  36. // build
  37. BuildVersion string
  38. BuildCommit string
  39. BuildStamp int64
  40. // Paths
  41. LogsPath string
  42. HomePath string
  43. DataPath string
  44. PluginsPath string
  45. // Log settings.
  46. LogModes []string
  47. LogConfigs []util.DynMap
  48. // Http server options
  49. Protocol Scheme
  50. Domain string
  51. HttpAddr, HttpPort string
  52. SshPort int
  53. CertFile, KeyFile string
  54. RouterLogging bool
  55. StaticRootPath string
  56. EnableGzip bool
  57. EnforceDomain bool
  58. // Security settings.
  59. SecretKey string
  60. LogInRememberDays int
  61. CookieUserName string
  62. CookieRememberName string
  63. DisableGravatar bool
  64. EmailCodeValidMinutes int
  65. DataProxyWhiteList map[string]bool
  66. // Snapshots
  67. ExternalSnapshotUrl string
  68. ExternalSnapshotName string
  69. ExternalEnabled bool
  70. // User settings
  71. AllowUserSignUp bool
  72. AllowUserOrgCreate bool
  73. AutoAssignOrg bool
  74. AutoAssignOrgRole string
  75. VerifyEmailEnabled bool
  76. LoginHint string
  77. DefaultTheme string
  78. // Http auth
  79. AdminUser string
  80. AdminPassword string
  81. AnonymousEnabled bool
  82. AnonymousOrgName string
  83. AnonymousOrgRole string
  84. // Auth proxy settings
  85. AuthProxyEnabled bool
  86. AuthProxyHeaderName string
  87. AuthProxyHeaderProperty string
  88. AuthProxyAutoSignUp bool
  89. // Basic Auth
  90. BasicAuthEnabled bool
  91. // Session settings.
  92. SessionOptions session.Options
  93. // Global setting objects.
  94. Cfg *ini.File
  95. ConfRootPath string
  96. IsWindows bool
  97. // PhantomJs Rendering
  98. ImagesDir string
  99. PhantomDir string
  100. // for logging purposes
  101. configFiles []string
  102. appliedCommandLineProperties []string
  103. appliedEnvOverrides []string
  104. ReportingEnabled bool
  105. CheckForUpdates bool
  106. GoogleAnalyticsId string
  107. GoogleTagManagerId string
  108. // LDAP
  109. LdapEnabled bool
  110. LdapConfigFile string
  111. // SMTP email settings
  112. Smtp SmtpSettings
  113. // QUOTA
  114. Quota QuotaSettings
  115. )
  116. type CommandLineArgs struct {
  117. Config string
  118. HomePath string
  119. Args []string
  120. }
  121. func init() {
  122. IsWindows = runtime.GOOS == "windows"
  123. log.NewLogger(0, "console", `{"level": 0, "formatting":true}`)
  124. }
  125. func parseAppUrlAndSubUrl(section *ini.Section) (string, string) {
  126. appUrl := section.Key("root_url").MustString("http://localhost:3000/")
  127. if appUrl[len(appUrl)-1] != '/' {
  128. appUrl += "/"
  129. }
  130. // Check if has app suburl.
  131. url, err := url.Parse(appUrl)
  132. if err != nil {
  133. log.Fatal(4, "Invalid root_url(%s): %s", appUrl, err)
  134. }
  135. appSubUrl := strings.TrimSuffix(url.Path, "/")
  136. return appUrl, appSubUrl
  137. }
  138. func ToAbsUrl(relativeUrl string) string {
  139. return AppUrl + relativeUrl
  140. }
  141. func shouldRedactKey(s string) bool {
  142. uppercased := strings.ToUpper(s)
  143. return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET")
  144. }
  145. func applyEnvVariableOverrides() {
  146. appliedEnvOverrides = make([]string, 0)
  147. for _, section := range Cfg.Sections() {
  148. for _, key := range section.Keys() {
  149. sectionName := strings.ToUpper(strings.Replace(section.Name(), ".", "_", -1))
  150. keyName := strings.ToUpper(strings.Replace(key.Name(), ".", "_", -1))
  151. envKey := fmt.Sprintf("GF_%s_%s", sectionName, keyName)
  152. envValue := os.Getenv(envKey)
  153. if len(envValue) > 0 {
  154. key.SetValue(envValue)
  155. if shouldRedactKey(envKey) {
  156. envValue = "*********"
  157. }
  158. appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, envValue))
  159. }
  160. }
  161. }
  162. }
  163. func applyCommandLineDefaultProperties(props map[string]string) {
  164. appliedCommandLineProperties = make([]string, 0)
  165. for _, section := range Cfg.Sections() {
  166. for _, key := range section.Keys() {
  167. keyString := fmt.Sprintf("default.%s.%s", section.Name(), key.Name())
  168. value, exists := props[keyString]
  169. if exists {
  170. key.SetValue(value)
  171. if shouldRedactKey(keyString) {
  172. value = "*********"
  173. }
  174. appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value))
  175. }
  176. }
  177. }
  178. }
  179. func applyCommandLineProperties(props map[string]string) {
  180. for _, section := range Cfg.Sections() {
  181. for _, key := range section.Keys() {
  182. keyString := fmt.Sprintf("%s.%s", section.Name(), key.Name())
  183. value, exists := props[keyString]
  184. if exists {
  185. key.SetValue(value)
  186. appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value))
  187. }
  188. }
  189. }
  190. }
  191. func getCommandLineProperties(args []string) map[string]string {
  192. props := make(map[string]string)
  193. for _, arg := range args {
  194. if !strings.HasPrefix(arg, "cfg:") {
  195. continue
  196. }
  197. trimmed := strings.TrimPrefix(arg, "cfg:")
  198. parts := strings.Split(trimmed, "=")
  199. if len(parts) != 2 {
  200. log.Fatal(3, "Invalid command line argument", arg)
  201. return nil
  202. }
  203. props[parts[0]] = parts[1]
  204. }
  205. return props
  206. }
  207. func makeAbsolute(path string, root string) string {
  208. if filepath.IsAbs(path) {
  209. return path
  210. }
  211. return filepath.Join(root, path)
  212. }
  213. func evalEnvVarExpression(value string) string {
  214. regex := regexp.MustCompile(`\${(\w+)}`)
  215. return regex.ReplaceAllStringFunc(value, func(envVar string) string {
  216. envVar = strings.TrimPrefix(envVar, "${")
  217. envVar = strings.TrimSuffix(envVar, "}")
  218. envValue := os.Getenv(envVar)
  219. // if env variable is hostname and it is emtpy use os.Hostname as default
  220. if envVar == "HOSTNAME" && envValue == "" {
  221. envValue, _ = os.Hostname()
  222. }
  223. return envValue
  224. })
  225. }
  226. func evalConfigValues() {
  227. for _, section := range Cfg.Sections() {
  228. for _, key := range section.Keys() {
  229. key.SetValue(evalEnvVarExpression(key.Value()))
  230. }
  231. }
  232. }
  233. func loadSpecifedConfigFile(configFile string) {
  234. if configFile == "" {
  235. configFile = filepath.Join(HomePath, "conf/custom.ini")
  236. // return without error if custom file does not exist
  237. if !pathExists(configFile) {
  238. return
  239. }
  240. }
  241. userConfig, err := ini.Load(configFile)
  242. userConfig.BlockMode = false
  243. if err != nil {
  244. log.Fatal(3, "Failed to parse %v, %v", configFile, err)
  245. }
  246. for _, section := range userConfig.Sections() {
  247. for _, key := range section.Keys() {
  248. if key.Value() == "" {
  249. continue
  250. }
  251. defaultSec, err := Cfg.GetSection(section.Name())
  252. if err != nil {
  253. defaultSec, _ = Cfg.NewSection(section.Name())
  254. }
  255. defaultKey, err := defaultSec.GetKey(key.Name())
  256. if err != nil {
  257. defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value())
  258. }
  259. defaultKey.SetValue(key.Value())
  260. }
  261. }
  262. configFiles = append(configFiles, configFile)
  263. }
  264. func loadConfiguration(args *CommandLineArgs) {
  265. var err error
  266. // load config defaults
  267. defaultConfigFile := path.Join(HomePath, "conf/defaults.ini")
  268. configFiles = append(configFiles, defaultConfigFile)
  269. Cfg, err = ini.Load(defaultConfigFile)
  270. Cfg.BlockMode = false
  271. if err != nil {
  272. log.Fatal(3, "Failed to parse defaults.ini, %v", err)
  273. }
  274. // command line props
  275. commandLineProps := getCommandLineProperties(args.Args)
  276. // load default overrides
  277. applyCommandLineDefaultProperties(commandLineProps)
  278. // init logging before specific config so we can log errors from here on
  279. DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
  280. initLogging(args)
  281. // load specified config file
  282. loadSpecifedConfigFile(args.Config)
  283. // apply environment overrides
  284. applyEnvVariableOverrides()
  285. // apply command line overrides
  286. applyCommandLineProperties(commandLineProps)
  287. // evaluate config values containing environment variables
  288. evalConfigValues()
  289. // update data path and logging config
  290. DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
  291. initLogging(args)
  292. }
  293. func pathExists(path string) bool {
  294. _, err := os.Stat(path)
  295. if err == nil {
  296. return true
  297. }
  298. if os.IsNotExist(err) {
  299. return false
  300. }
  301. return false
  302. }
  303. func setHomePath(args *CommandLineArgs) {
  304. if args.HomePath != "" {
  305. HomePath = args.HomePath
  306. return
  307. }
  308. HomePath, _ = filepath.Abs(".")
  309. // check if homepath is correct
  310. if pathExists(filepath.Join(HomePath, "conf/defaults.ini")) {
  311. return
  312. }
  313. // try down one path
  314. if pathExists(filepath.Join(HomePath, "../conf/defaults.ini")) {
  315. HomePath = filepath.Join(HomePath, "../")
  316. }
  317. }
  318. var skipStaticRootValidation bool = false
  319. func validateStaticRootPath() error {
  320. if skipStaticRootValidation {
  321. return nil
  322. }
  323. if _, err := os.Stat(path.Join(StaticRootPath, "css")); err == nil {
  324. return nil
  325. }
  326. if _, err := os.Stat(StaticRootPath + "_gen/css"); err == nil {
  327. StaticRootPath = StaticRootPath + "_gen"
  328. return nil
  329. }
  330. return fmt.Errorf("Failed to detect generated css or javascript files in static root (%s), have you executed default grunt task?", StaticRootPath)
  331. }
  332. // func readInstanceName() string {
  333. // hostname, _ := os.Hostname()
  334. // if hostname == "" {
  335. // hostname = "hostname_unknown"
  336. // }
  337. //
  338. // instanceName := Cfg.Section("").Key("instance_name").MustString("")
  339. // if instanceName = "" {
  340. // // set value as it might be used in other places
  341. // Cfg.Section("").Key("instance_name").SetValue(hostname)
  342. // instanceName = hostname
  343. // }
  344. //
  345. // return
  346. // }
  347. func NewConfigContext(args *CommandLineArgs) error {
  348. setHomePath(args)
  349. loadConfiguration(args)
  350. Env = Cfg.Section("").Key("app_mode").MustString("development")
  351. InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
  352. PluginsPath = Cfg.Section("paths").Key("plugins").String()
  353. server := Cfg.Section("server")
  354. AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
  355. Protocol = HTTP
  356. if server.Key("protocol").MustString("http") == "https" {
  357. Protocol = HTTPS
  358. CertFile = server.Key("cert_file").String()
  359. KeyFile = server.Key("cert_key").String()
  360. }
  361. Domain = server.Key("domain").MustString("localhost")
  362. HttpAddr = server.Key("http_addr").MustString("0.0.0.0")
  363. HttpPort = server.Key("http_port").MustString("3000")
  364. RouterLogging = server.Key("router_logging").MustBool(false)
  365. EnableGzip = server.Key("enable_gzip").MustBool(false)
  366. EnforceDomain = server.Key("enforce_domain").MustBool(false)
  367. StaticRootPath = makeAbsolute(server.Key("static_root_path").String(), HomePath)
  368. if err := validateStaticRootPath(); err != nil {
  369. return err
  370. }
  371. // read security settings
  372. security := Cfg.Section("security")
  373. SecretKey = security.Key("secret_key").String()
  374. LogInRememberDays = security.Key("login_remember_days").MustInt()
  375. CookieUserName = security.Key("cookie_username").String()
  376. CookieRememberName = security.Key("cookie_remember_name").String()
  377. DisableGravatar = security.Key("disable_gravatar").MustBool(true)
  378. // read snapshots settings
  379. snapshots := Cfg.Section("snapshots")
  380. ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String()
  381. ExternalSnapshotName = snapshots.Key("external_snapshot_name").String()
  382. ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
  383. // read data source proxy white list
  384. DataProxyWhiteList = make(map[string]bool)
  385. for _, hostAndIp := range security.Key("data_source_proxy_whitelist").Strings(" ") {
  386. DataProxyWhiteList[hostAndIp] = true
  387. }
  388. // admin
  389. AdminUser = security.Key("admin_user").String()
  390. AdminPassword = security.Key("admin_password").String()
  391. users := Cfg.Section("users")
  392. AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
  393. AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
  394. AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
  395. AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"})
  396. VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
  397. LoginHint = users.Key("login_hint").String()
  398. DefaultTheme = users.Key("default_theme").String()
  399. // anonymous access
  400. AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
  401. AnonymousOrgName = Cfg.Section("auth.anonymous").Key("org_name").String()
  402. AnonymousOrgRole = Cfg.Section("auth.anonymous").Key("org_role").String()
  403. // auth proxy
  404. authProxy := Cfg.Section("auth.proxy")
  405. AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)
  406. AuthProxyHeaderName = authProxy.Key("header_name").String()
  407. AuthProxyHeaderProperty = authProxy.Key("header_property").String()
  408. AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
  409. authBasic := Cfg.Section("auth.basic")
  410. BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
  411. // PhantomJS rendering
  412. ImagesDir = filepath.Join(DataPath, "png")
  413. PhantomDir = filepath.Join(HomePath, "vendor/phantomjs")
  414. analytics := Cfg.Section("analytics")
  415. ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
  416. CheckForUpdates = analytics.Key("check_for_updates").MustBool(true)
  417. GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
  418. GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
  419. ldapSec := Cfg.Section("auth.ldap")
  420. LdapEnabled = ldapSec.Key("enabled").MustBool(false)
  421. LdapConfigFile = ldapSec.Key("config_file").String()
  422. readSessionConfig()
  423. readSmtpSettings()
  424. readQuotaSettings()
  425. if VerifyEmailEnabled && !Smtp.Enabled {
  426. log.Warn("require_email_validation is enabled but smpt is disabled")
  427. }
  428. return nil
  429. }
  430. func readSessionConfig() {
  431. sec := Cfg.Section("session")
  432. SessionOptions = session.Options{}
  433. SessionOptions.Provider = sec.Key("provider").In("memory", []string{"memory", "file", "redis", "mysql", "postgres", "memcache"})
  434. SessionOptions.ProviderConfig = strings.Trim(sec.Key("provider_config").String(), "\" ")
  435. SessionOptions.CookieName = sec.Key("cookie_name").MustString("grafana_sess")
  436. SessionOptions.CookiePath = AppSubUrl
  437. SessionOptions.Secure = sec.Key("cookie_secure").MustBool()
  438. SessionOptions.Gclifetime = Cfg.Section("session").Key("gc_interval_time").MustInt64(86400)
  439. SessionOptions.Maxlifetime = Cfg.Section("session").Key("session_life_time").MustInt64(86400)
  440. SessionOptions.IDLength = 16
  441. if SessionOptions.Provider == "file" {
  442. SessionOptions.ProviderConfig = makeAbsolute(SessionOptions.ProviderConfig, DataPath)
  443. os.MkdirAll(path.Dir(SessionOptions.ProviderConfig), os.ModePerm)
  444. }
  445. if SessionOptions.CookiePath == "" {
  446. SessionOptions.CookiePath = "/"
  447. }
  448. }
  449. var logLevels = map[string]int{
  450. "Trace": 0,
  451. "Debug": 1,
  452. "Info": 2,
  453. "Warn": 3,
  454. "Error": 4,
  455. "Critical": 5,
  456. }
  457. func getLogLevel(key string, defaultName string) (string, int) {
  458. levelName := Cfg.Section(key).Key("level").In(defaultName, []string{"Trace", "Debug", "Info", "Warn", "Error", "Critical"})
  459. level, ok := logLevels[levelName]
  460. if !ok {
  461. log.Fatal(4, "Unknown log level: %s", levelName)
  462. }
  463. return levelName, level
  464. }
  465. func initLogging(args *CommandLineArgs) {
  466. //close any existing log handlers.
  467. log.Close()
  468. // Get and check log mode.
  469. LogModes = strings.Split(Cfg.Section("log").Key("mode").MustString("console"), ",")
  470. LogsPath = makeAbsolute(Cfg.Section("paths").Key("logs").String(), HomePath)
  471. defaultLevelName, _ := getLogLevel("log", "Info")
  472. LogConfigs = make([]util.DynMap, len(LogModes))
  473. for i, mode := range LogModes {
  474. mode = strings.TrimSpace(mode)
  475. sec, err := Cfg.GetSection("log." + mode)
  476. if err != nil {
  477. log.Fatal(4, "Unknown log mode: %s", mode)
  478. }
  479. // Log level.
  480. _, level := getLogLevel("log."+mode, defaultLevelName)
  481. // Generate log configuration.
  482. switch mode {
  483. case "console":
  484. formatting := sec.Key("formatting").MustBool(true)
  485. LogConfigs[i] = util.DynMap{
  486. "level": level,
  487. "formatting": formatting,
  488. }
  489. case "file":
  490. logPath := sec.Key("file_name").MustString(filepath.Join(LogsPath, "grafana.log"))
  491. os.MkdirAll(filepath.Dir(logPath), os.ModePerm)
  492. LogConfigs[i] = util.DynMap{
  493. "level": level,
  494. "filename": logPath,
  495. "rotate": sec.Key("log_rotate").MustBool(true),
  496. "maxlines": sec.Key("max_lines").MustInt(1000000),
  497. "maxsize": 1 << uint(sec.Key("max_size_shift").MustInt(28)),
  498. "daily": sec.Key("daily_rotate").MustBool(true),
  499. "maxdays": sec.Key("max_days").MustInt(7),
  500. }
  501. case "conn":
  502. LogConfigs[i] = util.DynMap{
  503. "level": level,
  504. "reconnectOnMsg": sec.Key("reconnect_on_msg").MustBool(),
  505. "reconnect": sec.Key("reconnect").MustBool(),
  506. "net": sec.Key("protocol").In("tcp", []string{"tcp", "unix", "udp"}),
  507. "addr": sec.Key("addr").MustString(":7020"),
  508. }
  509. case "smtp":
  510. LogConfigs[i] = util.DynMap{
  511. "level": level,
  512. "user": sec.Key("user").MustString("example@example.com"),
  513. "passwd": sec.Key("passwd").MustString("******"),
  514. "host": sec.Key("host").MustString("127.0.0.1:25"),
  515. "receivers": sec.Key("receivers").MustString("[]"),
  516. "subject": sec.Key("subject").MustString("Diagnostic message from serve"),
  517. }
  518. case "database":
  519. LogConfigs[i] = util.DynMap{
  520. "level": level,
  521. "driver": sec.Key("driver").String(),
  522. "conn": sec.Key("conn").String(),
  523. }
  524. case "syslog":
  525. LogConfigs[i] = util.DynMap{
  526. "level": level,
  527. "network": sec.Key("network").MustString(""),
  528. "address": sec.Key("address").MustString(""),
  529. "facility": sec.Key("facility").MustString("local7"),
  530. "tag": sec.Key("tag").MustString(""),
  531. }
  532. }
  533. cfgJsonBytes, _ := json.Marshal(LogConfigs[i])
  534. log.NewLogger(Cfg.Section("log").Key("buffer_len").MustInt64(10000), mode, string(cfgJsonBytes))
  535. }
  536. }
  537. func LogConfigurationInfo() {
  538. var text bytes.Buffer
  539. text.WriteString("Configuration Info\n")
  540. text.WriteString("Config files:\n")
  541. for i, file := range configFiles {
  542. text.WriteString(fmt.Sprintf(" [%d]: %s\n", i, file))
  543. }
  544. if len(appliedCommandLineProperties) > 0 {
  545. text.WriteString("Command lines overrides:\n")
  546. for i, prop := range appliedCommandLineProperties {
  547. text.WriteString(fmt.Sprintf(" [%d]: %s\n", i, prop))
  548. }
  549. }
  550. if len(appliedEnvOverrides) > 0 {
  551. text.WriteString("\tEnvironment variables used:\n")
  552. for i, prop := range appliedEnvOverrides {
  553. text.WriteString(fmt.Sprintf(" [%d]: %s\n", i, prop))
  554. }
  555. }
  556. text.WriteString("Paths:\n")
  557. text.WriteString(fmt.Sprintf(" home: %s\n", HomePath))
  558. text.WriteString(fmt.Sprintf(" data: %s\n", DataPath))
  559. text.WriteString(fmt.Sprintf(" logs: %s\n", LogsPath))
  560. text.WriteString(fmt.Sprintf(" plugins: %s\n", PluginsPath))
  561. log.Info(text.String())
  562. }