Как вести журнал трассировки в Go с очень низкой стоимостью для отключенных операторов журнала
Полезно оставлять низкоуровневые заявления об отладке/трассировке в критических путях, чтобы их можно было включить с помощью конфигурации времени выполнения. Идея заключается в том, что вы никогда не включаете такой журнал в производство (это может привести к нарушению производительности), но вы можете включить его в производственной среде (например, производственная система отключена для отладки или тестовой системы, которая настроена точно так же, как и в производственной системе).
В этом типе ведения журнала есть специальное требование: стоимость нажатия на отключенный оператор журнала на критическом пути должна быть очень низкой: в идеале это один логический тест.
В C/С++ я бы сделал это с помощью макроса LOG, который не оценивает ни один из его аргументов, пока не проверит флажок. Только если это разрешено, мы вызываем некоторую вспомогательную функцию для форматирования и доставки сообщения журнала.
Итак, как это сделать в Go?
Использование io.Discard с log.Logger - это не стартер: он полностью форматирует сообщение журнала каждый раз, прежде чем выбросить его, если он отключен.
Моя первая мысль -
type EnabledLogger struct { Enabled bool; delegate *log.Logger;... }
// Implement the log.Logger interface methods as:
func (e EnabledLogger) Print(...) { if e.Enabled { e.delegate.Output(...) } }
Это близко. Если я скажу:
myEnabledLogger.Printf("foo %v: %v", x, y)
Он не будет форматировать или записывать что-либо, если отключено, но будет оценивать аргументы x и y. Это нормально для базовых типов или указателей, а не для произвольных вызовов функций - например. для создания значений, которые не имеют метода String().
Я вижу два пути:
Типы обертки для отсрочки вызова:
type Stringify { x *Thing }
func (s Stringify) String() { return someStringFn(s.x) }
enabledLogger.Printf("foo %v", Stringify{&aThing})
Оберните все в ручную проверку:
if enabledLog.Enabled {
enabledLog.Printf("foo %v", someStringFn(x))
}
Оба являются многословными и подверженными ошибкам, слишком легко для кого-то забыть шаг и спокойно ввести неприятную регрессию производительности.
Я начинаю любить Go. Скажите, пожалуйста, это может решить эту проблему:)
Ответы
Ответ 1
Все аргументы в Go гарантированно оцениваются, и в языке нет определенных макросов препроцессора, поэтому вы можете сделать только пару вещей.
Чтобы избежать дорогостоящих вызовов функций в аргументах ведения журнала, используйте интерфейсы fmt.Stringer
и fmt.GoStringer
для отсрочки форматирования до тех пор, пока функция не будет выполнена. Таким образом, вы можете передавать простые типы в Printf
. Вы можете расширить этот шаблон самостоятельно с помощью настраиваемого регистратора, который также проверяет различные интерфейсы. Это то, что вы используете в своем примере Stringify
, и вы можете реально применять его только при проверке кода и модульных тестах.
type LogFormatter interface {
LogFormat() string
}
// inside the logger
if i, ok := i.(LogFormatter); ok {
fmt.Println(i.LogFormat())
}
Вы также можете поменять весь журнал на время выполнения через интерфейс регистратора или полностью заменить его на время сборки с помощью ограничений сборки, но по-прежнему требует обеспечения того, чтобы дорогие вызовы не вставлялись в аргументы ведения журнала.
Другой шаблон, используемый некоторыми пакетами, такими как glog, - сделать сам Logger bool. Это не полностью устраняет многословие, но делает его немного более кратким.
type Log bool
func (l Log) Println(args ...interface{}) {
fmt.Println(args...)
}
var debug Log = false
if debug {
debug.Println("DEBUGGING")
}
Ближе всего вы можете перейти к предварительной обработке макросов, чтобы использовать генерацию кода. Это не будет работать для настраиваемой трассировки времени исполнения, но может по крайней мере обеспечить отдельную сборку отладки, которая может быть вставлена на место, когда это необходимо. Это может быть так же просто, как gofmt -r
, создание файла с помощью text/template
или полного поколения путем анализа кода и построения AST.
Ответ 2
Прежде всего, я должен сказать, что лично я пошёл бы с самым простым решением и просто использовал if
. Но если вы действительно хотите сделать это надежно, вы можете использовать анонимные функции:
type verbose bool
func (v verbose) Run(f func()) {
if v {
f()
}
}
Используйте его следующим образом:
v := verbose(false)
v.Run(func() {
log.Println("No Print")
})
Пример площадки с дорогими вызовами: http://play.golang.org/p/9pm2HoQQ8A.