Если hslogger — это такая навороченная штуковина с уровнями логирования, какими-то деревьями, форматерами, хендлерами и syslog’ом, то пакет fast-logger предназначен для очень простого, но эффективного логирования в файлики. Эффективного, потому что fast-logger скейлится на многоядерных системах за счет создания отдельных буферов на каждое ядро процессора. Лог-записи попадают в различные буферы в зависимости от id нитки, делающей запись. При заполнении буфер сбрасывается на диск. Принцип такой же, как при шардинге — данные разделяются, нитки меньше мешают друг другу.
Еще одна причина, по которой fast-logger заслуживает внимания, заключается в том, что он по умолчанию используется в WAI . Если вы используете Scotty или Yesod , то автоматически тащите в качестве зависимости fast-logger. А раз в вашем приложении уже есть какой-то логер, почему бы не воспользоваться им, вместо того, чтобы тащить еще один логер и как-то конвертировать типы fast-logger’а в типы, с которыми работает этот другой логер?
В первом приближении fast-logger предельно прост в использовании:
ghci> loggerSet <- newFileLoggerSet defaultBufSize «./temp/info.log»
ghci> pushLogStr loggerSet «log entry 1n»
ghci> pushLogStr loggerSet «log entry 2n»
ghci> flushLogStr loggerSet
Но тут есть несколько проблем. Во-первых, в отличие от hslogger, fast-logger не предлагает из коробки какого-то глобального логера или монады, в которую он был бы обернут . Во-вторых, в fast-logger нет ни уровней (debug, info, warning, error), ни форматеров. То есть, если вы хотите писать в лог таймстампы, придется все закодить самому. С одной стороны, это все очень плохо, потому что нужно думать и что-то делать. Но с другой стороны, fast-logger предоставляет программисту больше гибкости. Можно заточить его под наши конкретные нужды.
На практике все не так плохо, как кажется. Реализация глобального логера с таймстампами занимает 15 строк кода:
globalLogger :: LoggerSet
globalLogger =
unsafePerformIO $ do
conf <- loadConfig
logFile <- lookupDefault «./info.log» conf «logging.file»
buffSize <- lookupDefault defaultBufSize conf «logging.buff_size»
newFileLoggerSet buffSize logFile
writeLog :: BS . ByteString -> IO ( )
writeLog str = do
utcTime <- getCurrentTime
let timestampStr = formatISO8601Millis utcTime
timestamp = BS . pack timestampStr
logEntry = timestamp <> » » <> str <> » n »
pushLogStr globalLogger $ toLogStr logEntry
Для чтения конфига здесь используется пакет configurator , а для форматирования времени в ISO 8601 — пакет iso8601-time . Объяснение приема с глобальными переменными в Haskell через unsafePerformIO можно найти здесь (пункт 3). Это стандартный и безопасный прием в мире Haskell. В пакете hslogger делается точно так же.
Еще из коробки fast-logger не делает ротацию логов. Однако она также легко прикручивается:
ghci> rotate logspec >> renewLoggerSet loggerSet
Перед вызовом renewLoggerSet обязательно следует произвести ротацию, иначе будет брошено исключение:
*** Exception: openFile: resource busy (file is locked)
Помимо функции rotate есть еще функция check, которая не производит ротацию, а только проверяет, что ее можно выполнить — хватает прав и так далее.
Как часто нужно делать ротацию, опять же, решать вам. В следующем примере ротация выполняется один раз в сутки:
forkLogRotator conf = do
_ <- forkIO $ do
writeLog «Log rotator started»
— читаем из конфига logFile, fileSize, backupNumber, flushInt
— …
let logSpec = FileLogSpec
{ log _ file = logFile
, log _ file _ size = fileSize
, log _ backup _ number = backupNumber
}
forever $
catchAny ( getCurrentTime >>= logRotatorPayload flushInt logSpec )
( e -> void ( writeLog $ «LogRot: » <> BS . pack ( show e ) ) )
return ( )
logRotatorPayload :: Int -> FileLogSpec -> UTCTime -> IO ( )
logRotatorPayload flushInterval logSpec oldTime = do
newTime <- getCurrentTime
let [ newYMD @ ( _,_,_ ) , oldYMD ] = map DT . toGregorian’ [ newTime , oldTime ]
flushLogStr globalLogger
when ( newYMD /= oldYMD ) $ do
writeLog «Rotating logs…»
rotate logSpec
renewLoggerSet globalLogger
sleepSeconds flushInterval
logRotatorPayload flushInterval logSpec newTime
Создается отдельная нитка , которая просыпается один раз во flushInterval секунд. Каждый раз, просыпаясь, нитка делает flush логов на диск. Кроме того, если выясняется, что с момента последнего пробуждения изменилась текущая дата, нитка выполняет ротацию логов. В случае возникновения любого исключения нитка записывает информацию об исключении в лог и продолжает свою работу. Для поимки всех исключений используется функция catchAny из замечательного пакета enclosed-exceptions . Кстати, к вопросу о том, «как же в Haskell делаются долгоживущие процессы».
Напоследок посмотрим, как при помощи fast-logger можно прикрутить к Scotty логирование всех запросов. Для решения подобного рода задач предназначены так называемые middleware. Это очень мощных механизм. С помощью middleware можно произвольным образом обрабатывать и менять как запросы пользователя, так ответы сервера. В том числе middleware позволяют прикручивать сжатие при помощи gzip, реализовывать какую-то хитрую авторизацию и так далее. Для прикручивания нового middleware в Scotty есть одноименная функция:
… где определение типа Middleware следующее:
type Application = Request
-> (Response -> IO ResponseReceived)
-> IO ResponseReceived
На первый взгляд все это может показаться несколько запутанным, но в действительности тут нет ничего кроме обыкновенных функций высших порядков. Приложение (Application) — это функция, которая принимает запрос (Request), функцию отправки ответа (Response) и, видимо, обязательно должна вызывать эту функцию, чтобы вернуть ожидаемый тип (IO ResponseReceived). А Middleware представляет собой функцию, которая принимает Application и возвращает Application. Которые в свою очередь, напоминаю, являются просто функциями.
Проще всего понять, как работаю middleware, посмотрев на пример простейшего middleware, который ничего не делает:
dummyMiddleware app req sendResp =
app req ( resp -> do
let resp’ = id resp — не преобразуем
sendResp resp’
)
Если все еще непонятно, просто помедитируйте над этим примером подольше, тут правда нет ничего сложного.
Так вот, а теперь вернемся к логированию запросов. Есть такой пакет wai-extra , предоставляющий дюжину готовых middleware, в том числе RequestLogger, предназначенный для логирования запросов. Создание нового RequestLogger’а происходит при помощи функции mkRequestLogger:
В действии это выглядит следующим образом:
let logConf = def
{ destination = Callback ( writeLog . fromLogStr )
, outputFormat = CustomOutputFormat requestLoggingFmt
}
logger <- mkRequestLogger logConf
scotty port $ do
middleware logger
get «/api/v1.0/server_info» getServerInfo
… где функция requestLoggingFmt определена как-то так:
-> Request
-> Status
-> Maybe Integer
-> LogStr
requestLoggingFmt _ req _ _ =
let logMethod = requestMethod req
logPathInfo = rawPathInfo req
logQueryString = rawQueryString req
logClientAddr = remoteHost req
logHeaders = requestHeaders req
logBodyLenAdt = requestBodyLength req
logBodyLenStr = case logBodyLenAdt of
ChunkedBody -> «(chunked)»
KnownLength len -> show len
in toLogStr $ «Request from » <> BS . pack ( show logClientAddr ) <>
» » <> logMethod <> » » <> logPathInfo <>
logQueryString <>
«, headers: » <> BS . pack ( show logHeaders ) <>
«, body length: » <> BS . pack logBodyLenStr
Если вас пугают непонятные стрелочки <>
, ознакомьтесь с моей заметкой про моноиды .
Итак, подводя итог. Из коробки пакет fast-logger не дает всего и сразу, но только потому что он не знает, чем вы решили читать конфиги, предпочитаете ли вы монады или глобальные переменные, в каком формате вы хотите логировать время, как часто следует выполнять ротацию логов и так далее. На Hackage можно найти несколько попыток сделать как бы более удобную обертку над fast-logger, но все они, похоже, в итоге оказываются заточенными под нужды конкретных авторов. Помимо hslogger и fast-logger из интересных пакетов для логирования можно посоветовать rotating-log . Он совмещает в себе простоту fast-logger и способность самостоятельно выполнять ротацию логов. Впрочем, в отличие от fast-logger, он не оптимизирован под работу на многоядерных машинах.
Я подозреваю, что б о льшая часть читателей отвалилась на моменте, где я начал рассказывать о middleware, так что, кто дочитал заметку до конца, тот молодец. Как всегда, буду рад вашим вопросам и дополнениям.