tool/config/config.go (204 lines of code) (raw):
// Copyright (c) 2024 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"unicode"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/errc"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/util"
)
const (
EnvPrefix = "OTELTOOL_"
BuildConfFile = "conf.json"
)
type BuildConfig struct {
// RuleJsonFiles is the name of the rule file. It is used to tell instrument
// tool where to find the instrument rules. Multiple rules are separated by
// comma. e.g. -rule=rule1.json,rule2.json. By default, new rules are appended
// to default rules, i.e. -rule=rule1.json,rule2.json is exactly equivalent to
// -rule=default.json,rule1.json,rule2.json. But if you do want to disable
// default rules, you can configure -disabledefault flag in advance.
RuleJsonFiles string
// Log specifies the log file path. If not set, log will be saved to file.
Log string
// Verbose true means print verbose log.
Verbose bool
// Debug true means debug mode.
Debug bool
// Restore true means restore all instrumentations.
Restore bool
// DisableDefault true means disable default rules.
DisableDefault bool
}
// This is the version of the tool, which will be printed when the -version flag
// is passed. This value is specified by the build system.
var ToolVersion = "1.0.0"
var conf *BuildConfig
func GetConf() *BuildConfig {
util.Assert(conf != nil, "build config is not initialized")
return conf
}
func (bc *BuildConfig) IsDisableDefault() bool {
return bc.DisableDefault
}
func (bc *BuildConfig) makeRuleAbs(file string) (string, error) {
if util.PathNotExists(file) {
return "", errc.New(errc.ErrNotExist, file)
}
file, err := filepath.Abs(file)
if err != nil {
return "", errc.New(errc.ErrAbsPath, err.Error())
}
return file, nil
}
func (bc *BuildConfig) parseRuleFiles() error {
if util.InInstrument() {
return nil
}
// Get absolute path of rule file, otherwise instrument will not
// be able to find the rule file because it is running in different
// working directory.
if bc.RuleJsonFiles == "" {
return nil
}
if strings.Contains(bc.RuleJsonFiles, ",") {
files := strings.Split(bc.RuleJsonFiles, ",")
for i, file := range files {
f, err := bc.makeRuleAbs(file)
if err != nil {
return err
}
files[i] = f
}
bc.RuleJsonFiles = strings.Join(files, ",")
} else {
f, err := bc.makeRuleAbs(bc.RuleJsonFiles)
if err != nil {
return err
}
bc.RuleJsonFiles = f
}
return nil
}
func getConfPath(name string) string {
return util.GetTempBuildDirWith(name)
}
func storeConfig(bc *BuildConfig) error {
util.Assert(bc != nil, "build config is not initialized")
file := getConfPath(BuildConfFile)
bs, err := json.Marshal(bc)
if err != nil {
return errc.New(errc.ErrInvalidJSON, err.Error())
}
_, err = util.WriteFile(file, string(bs))
if err != nil {
return err
}
return nil
}
func loadConfig() (*BuildConfig, error) {
util.Assert(conf == nil, "build config is already initialized")
// If the build config file does not exist, return a default build config
confFile := getConfPath(BuildConfFile)
if util.PathNotExists(confFile) {
return &BuildConfig{}, nil
}
// Load build config from json file
file := getConfPath(BuildConfFile)
data, err := util.ReadFile(file)
if err != nil {
return &BuildConfig{}, err
}
bc := &BuildConfig{}
err = json.Unmarshal([]byte(data), bc)
if err != nil {
return nil, errc.New(errc.ErrInvalidJSON, err.Error())
}
return bc, nil
}
func toUpperSnakeCase(input string) string {
var result []rune
for i, char := range input {
if unicode.IsUpper(char) {
if i != 0 {
result = append(result, '_')
}
result = append(result, unicode.ToUpper(char))
} else {
result = append(result, unicode.ToUpper(char))
}
}
return string(result)
}
func loadConfigFromEnv(conf *BuildConfig) {
// Environment variables are able to overwrite the config items even if the
// config file sets them. The environment variable name is the upper snake
// case of the config item name, prefixed with "OTELTOOL_". For example, the
// environment variable for "Log" is "OTELTOOL_LOG".
typ := reflect.TypeOf(*conf)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
envKey := fmt.Sprintf("%s%s", EnvPrefix, toUpperSnakeCase(field.Name))
envVal := os.Getenv(envKey)
if envVal != "" {
if util.InPreprocess() {
util.Log("Overwrite config %s with environment variable %s",
field.Name, envKey)
}
v := reflect.ValueOf(conf).Elem()
f := v.FieldByName(field.Name)
switch f.Kind() {
case reflect.Bool:
f.SetBool(envVal == "true")
case reflect.String:
f.SetString(envVal)
default:
util.ShouldNotReachHere()
}
}
}
}
func InitConfig() (err error) {
// Load build config from json file
conf, err = loadConfig()
if err != nil {
return err
}
loadConfigFromEnv(conf)
err = conf.parseRuleFiles()
if err != nil {
return err
}
mode := os.O_WRONLY | os.O_APPEND
if util.InPreprocess() {
// We always create log file in preprocess phase, but in further
// instrument phase, we append log content to the existing file.
mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
}
if conf.Log == "" {
// Redirect log to file if flag is not set
debugLogPath := util.GetPreprocessLogPath(util.DebugLogFile)
debugLog, _ := os.OpenFile(debugLogPath, mode, 0777)
if debugLog != nil {
util.SetLogger(debugLog)
}
} else {
// Otherwise, log to the specified file
logFile, err := os.OpenFile(conf.Log, mode, 0777)
if err != nil {
return errc.New(errc.ErrOpenFile, err.Error())
}
util.SetLogger(logFile)
}
return nil
}
func PrintVersion() error {
name, err := util.GetToolName()
if err != nil {
return err
}
fmt.Printf("%s version %s\n", name, ToolVersion)
return nil
}
func Configure() error {
// Parse command line flags to get build config
bc, err := loadConfig()
if err != nil {
bc = &BuildConfig{}
}
flag.StringVar(&bc.Log, "log", bc.Log,
"Log file path. If not set, log will be saved to file.")
flag.BoolVar(&bc.Verbose, "verbose", bc.Verbose,
"Print verbose log")
flag.BoolVar(&bc.Debug, "debug", bc.Debug,
"Enable debug mode, leave temporary files for debugging")
flag.BoolVar(&bc.Restore, "restore", bc.Restore,
"Restore all instrumentations")
flag.StringVar(&bc.RuleJsonFiles, "rule", bc.RuleJsonFiles,
"Use custom.json rules. Multiple rules are separated by comma.")
flag.BoolVar(&bc.DisableDefault, "disabledefault", bc.DisableDefault,
"Disable default rules")
flag.CommandLine.Parse(os.Args[2:])
util.Log("Configured in %s", getConfPath(BuildConfFile))
// Store build config for future phases
err = storeConfig(bc)
if err != nil {
return err
}
return nil
}