Zen and the Art of Craft CMS Log File Reading
Like a black box recording, log files contain precious forensic information for when you have to diagnose problems. Let’s learn the art of reading them
Andrew Welch / nystudio107
The age old practice of logging is used in programming to provide a record of not just errors that happen, but also general status information. It’s this context that makes them useful when you’re trying to figure out what happened.
Or more often than not, <span></span><span>“</span>What went wrong?”
Log files are your “black box” recording of what happened, allowing you to piece things together post-facto when something goes wrong.
Craft CMS Debugging
Craft CMS is no different, in that it creates a number of logs to help you. If you’re debugging in local dev, I recommend using the Debug Toolbar as discussed in the Profiling your Website with Craft CMS 3’s Debug Toolbar article:
The Debug Toolbar provides access to the Craft CMS logs in a nice GUI that allows you to easily search, sort, and filter in a variety of ways. For more information on using the Debug Toolbar, please see the Profiling your Website with Craft CMS 3’s Debug Toolbar article.
In many cases you’ll need to debug a problem that you only have log files to go on, because:
- You can’t reproduce the issue locally
- You don’t have any steps to reproduce the issue
- The issue happened on someone else’s website
In these cases, we’ll be relying on log files to find out what happened, so let’s have a look what the various log files look like in Craft CMS.
Craft CMS Log Files
Craft CMS creates a a number of log files in the storage/logs/ directory. The format of the information logged in each is the same, but the files are segregated to make it easier to read through them.
Here are the log files that Craft CMS creates in the storage/logs/ directory:
- web.log — this is the primary web log file where most logging from Craft CMS & plugins will end up
- web404s.log — this is where the details of any 404 not found http requests are logged; they are logged separately to keep from cluttering up the web.log
- console.log — this console or CLI version of web.log; when you run Craft via the CLI, this is where most logging from Craft CMS & plugins will end up
- queue.log — this is where any logging from queue jobs (such as “Resaving Elements”) ends up, whether run via the web or CLI
- phperrors.log — this is where hard PHP errors that couldn’t be handled by Yii2’s exception handling are stored
Even if you use a third party logging service such as PaperTrail or Sentry to redirect or aggregate your Craft CMS logs, the information in this article will still help you digest the Craft logs.
A typical storage/logs/ directory might look like this:
vagrant@homestead ~/sites/craft3/storage/logs $ ls -al
total 63632
drwxr-xr-x 12 vagrant vagrant 384 Jul 20 2019 .
drwxr-xr-x 10 vagrant vagrant 320 Feb 16 03:11 ..
-rw-r--r-- 1 vagrant vagrant 180660 Jul 17 03:03 console.log
-rw-r--r-- 1 vagrant vagrant 389886 Jul 17 02:05 queue.log
-rw-r--r-- 1 vagrant vagrant 132927 Jul 20 2019 web-404s.log
-rw-r--r-- 1 vagrant vagrant 8072957 Jul 20 2019 web.log
-rw-r--r-- 1 vagrant vagrant 10497035 Jul 18 20:37 web.log.1
-rw-r--r-- 1 vagrant vagrant 10578305 Jul 18 20:37 web.log.2
-rw-r--r-- 1 vagrant vagrant 10555544 Jul 18 20:37 web.log.3
-rw-r--r-- 1 vagrant vagrant 10555546 Jul 18 20:37 web.log.4
-rw-r--r-- 1 vagrant vagrant 10578305 Jul 18 20:37 web.log.5
Note that all of the logs rotate as soon as they reach the maximum file size of 10,240KB, up to the a maximum of 5 rotated logs (both default maximums can be changed via app.php config). So in the above example, the web.log.5 file is the oldest log file, and web.log is the current log file.
Dissecting a Craft CMS Log File Line
Let’s dissect a line from my web.log file:
2019-03-04 00:08:49 [-][1][mivsgmhfgalsu5k7cv1f0q8m13][trace][yii\base\Module::getModule] Loading module: site-module
- 2019-03-04 00:08:49 — Date and time of the logged message (formatted as Y-m-d H:i:s in server time)
- [-] — The IP address of the client, by default redacted to - for GDPR compliance
- [1] — The user ID of the currently logged in user (or - if no user is logged in)
- [mivsgmhfgalsu5k7cv1f0q8m13] — The session ID (or - if no session exists)
- [trace] — The level of the log message (n.b. trace is aka debug, see below)
- [yii\base\Module::getModule] — The category of the log message, usually set to the class::method that called the logging, via METHOD
- Loading module: site-module — The log message itself
Let’s have a look at the log levels you’ll encounter:
- debug (aka trace)- a message to trace how a piece of code runs. This is mainly for development use.
- info — a message that conveys some useful information.
- warning — a warning message that indicates something unexpected has happened.
- error — a fatal error that should be investigated as soon as possible.
- profile — a special type of message logging that is used to measure the time taken by certain code blocks
It’s important to note that unless devMode is on, Craft will only log levels warning, error & profile. We don’t cover the profile level here, for more information on that, check out the Profiling your Website with Craft CMS 3’s Debug Toolbar article.
Since Craft CMS is based on the Yii2 framework, you can read up on the gory details of Yii2 Logging here.
Now that we have a grounding on what is in the Craft CMS logs, let’s dive into how to read the tea leaves.
Divining Meaning from the Craft CMS log files
So great, we’re armed with the knowledge of what the various log files are in Craft CMS, and also what each log line looks like. But there’s a ton of information logged by Craft, especially if devMode is on.
If devMode is enabled, in addition to error, warning, & profile log levels, we’re also logging trace (aka debug) and info log levels too.
While in theory this may not seem like it’s not such a big deal adding 2 more log levels, in practice it’s actually adding heaps of information to the logs files. Part of the reason is that every database query is now logged:
2019-03-04 00:08:49 [-][-][mivsgmhfgalsu5k7cv1f0q8m13][profile begin][yii\db\Command::query] SELECT
d.nspname AS table_schema,
c.relname AS table_name,
a.attname AS column_name,
COALESCE(td.typname, tb.typname, t.typname) AS data_type,
COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
a.attlen AS character_maximum_length,
pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
a.atttypmod AS modifier,
a.attnotnull = false AS is_nullable,
CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) AS is_autoinc,
CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
ELSE NULL
END AS enum_values,
CASE atttypid
WHEN 21 /*int2*/ THEN 16
WHEN 23 /*int4*/ THEN 32
WHEN 20 /*int8*/ THEN 64
WHEN 1700 /*numeric*/ THEN
CASE WHEN atttypmod = -1
THEN null
ELSE ((atttypmod - 4) >> 16) & 65535
END
WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
ELSE null
END AS numeric_precision,
CASE
WHEN atttypid IN (21, 23, 20) THEN 0
WHEN atttypid IN (1700) THEN
CASE
WHEN atttypmod = -1 THEN null
ELSE (atttypmod - 4) & 65535
END
ELSE null
END AS numeric_scale,
CAST(
information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
AS numeric
) AS size,
a.attnum = any (ct.conkey) as is_pkey,
COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
FROM
pg_class c
LEFT JOIN pg_attribute a ON a.attrelid = c.oid
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
LEFT JOIN pg_type t ON a.atttypid = t.oid
LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid OR t.typbasetype > 0 AND t.typbasetype = tb.oid
LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
WHERE
a.attnum > 0 AND t.typname != ''
AND c.relname = 'retour_redirects'
AND d.nspname = 'public'
ORDER BY
a.attnum;
And there are lots of them, so the pile you have to search through gets large quick. This “log everything” strategy is actually very useful when you’re doing forensics to figure out what happened. The more context the better.
But it’s also a bit like looking for a needle in a haystack to find what’s really relevant to you.
So let’s have a look at a few strategies that can help find what we’re looking for.
1. DELETE THE LOGS
If you’re attempting to reproduce an issue (especially in local dev), one way to make sifting through piles of information easier is to have less information to sift through!
- Navigate to the frontend page or CP page where you can reproduce the issue
- Delete all of the log files either via GUI or via rm -f storage/logs/* via shell
- Reproduce the issue
Now your log files will be pared down to mostly relevant information, and it should make your job easier to do. If you need to then pass the issue along to Pixel & Tonic or a plugin developer, this also gives you concise and relevant logs for them.
2. SEARCH ON [ERROR]
If you’re trying to find an error that’s reported by Craft or a plugin, usually it’ll be logged with log level error.
But if you just search on error you’ll find tons of matches. Instead, use the knowledge we learned earlier to search on the log level [error] (with the []) This should help you find just the lines where an actual error is thrown.
3. CRAWL THE STACK
If an actual error is thrown, usually there will be what’s called a “stack trace” right after the error line in your log file. Worst case, you can always just search the log file for stack trace. He’s a stack trace from an actual issue I was trying to track down:
Stack trace:
#0 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/helpers/Assets.php(93): craft\elements\Asset->getFolder()
#1 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/services/Assets.php(579): craft\helpers\Assets::generateUrl(Object(craft\volumes\Local), Object(craft\elements\Asset))
#2 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/elements/Asset.php(773): craft\services\Assets->getAssetUrl(Object(craft\elements\Asset), NULL)
#3 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/Component.php(139): craft\elements\Asset->getUrl()
#4 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/base/Element.php(879): yii\base\Component->__get('url')
#5 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/elements/Asset.php(582): craft\base\Element->__get('url')
#6 /home/vagrant/webdev/craft/craft-retour/src/Retour.php(306): craft\elements\Asset->__get('url')
#7 [internal function]: nystudio107\retour\Retour->nystudio107\retour\{closure}(Object(craft\events\ElementEvent))
#8 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/Event.php(312): call_user_func(Object(Closure), Object(craft\events\ElementEvent))
#9 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/Component.php(636): yii\base\Event::trigger('craft\\services\\...', 'beforeSaveEleme...', Object(craft\events\ElementEvent))
#10 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/services/Elements.php(502): yii\base\Component->trigger('beforeSaveEleme...', Object(craft\events\ElementEvent))
#11 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/controllers/AssetsController.php(118): craft\services\Elements->saveElement(Object(craft\elements\Asset))
#12 [internal function]: craft\controllers\AssetsController->actionSaveAsset()
#13 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array(Array, Array)
#14 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/Controller.php(157): yii\base\InlineAction->runWithParams(Array)
#15 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/web/Controller.php(187): yii\base\Controller->runAction('save-asset', Array)
#16 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/Module.php(528): craft\web\Controller->runAction('save-asset', Array)
#17 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/web/Application.php(298): yii\base\Module->runAction('assets/save-ass...', Array)
#18 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/web/Application.php(565): craft\web\Application->runAction('assets/save-ass...', Array)
#19 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/web/Application.php(277): craft\web\Application->_processActionRequest(Object(craft\web\Request))
#20 /home/vagrant/sites/craft3/vendor/yiisoft/yii2/base/Application.php(386): craft\web\Application->handleRequest(Object(craft\web\Request))
#21 /home/vagrant/sites/craft3/web/index.php(24): yii\base\Application->run()
#22 {main}
This may look a little scary, but we can break it down into something easily understandable.
First of all, stack traces are a list of the functions or methods that are called, in reverse order, until the error or exception happened.
A Stack Trace is turn by turn directions to your code’s car crash
The bottommost item in the stack trace labeled #22 {main} is where the execution started, and it ends up at #0 where the error was caught:
#0 /home/vagrant/sites/craft3/vendor/craftcms/cms/src/helpers/Assets.php(93): craft\elements\Asset->getFolder()
Let’s break down what this line is telling us:
- #0 — this is the 0th line in the stack trace before the error occurred; think “ground zero”
- /home/vagrant/sites/craft3/vendor/craftcms/cms/src/helpers/Assets.php — the path to the file in which the error occurred
- (93) — the line number in the aforementioned file where the error occurred
- craft\elements\Asset->getFolder() — the line of code where the error or exception occurred
So great! This gives us a pretty good idea of what happened, and we can look back through the stack trace to see all of the functions/methods that were called leading up to the error.
4. LOOK IN THE RIGHT PLACE
If you’re attempting to diagnose a problem, make sure you’re looking at the right log file. While this may seem basic, it’s an important thing to keep in mind.
For instance, if you’re attempting to diagnose an issue with a queue job like “Resaving Elements”, any errors would be logged in queue.log, not the normal web.log file.
Similarly, if you’re attempting to debug a Console Command, make sure you look in console.log, not the normal web.log file.
5. USE CONSOLE.APP
If you’re using a Mac, you might consider using Console.app (in Applications/Utilities) when you’re doing some serious log file spelunking. It’s a lean, mean, log-eating machine.
Your text editor of choice is probably fantastic for editing code, but it’s usually only passable for parsing through masses of data in a log file.
A really handy feature is that you can use Console.app’s filter at the top of the window to show only lines that contain retour in the example screenshot.
Then you can use its search feature to search only the resulting filter lines of the log file for specific text, error in this case.
Console.app really is designed for parsing log files; if you double click on any file with a .log suffix, Console.app opens by default.
6. USE THE COMMAND LINE
If you feel comfortable using the CLI, here are a few commands that you may file useful:
- grep -F '[error]' web.log — search the web.log file for the fixed string [error] and display matching lines. More info…
- grep -C 2 -F '[error]' web.log — adding the -C 2 option to the above command causes it to also print out the 2 lines before and 2 lines after the match. More info…
- tail -f web.log | grep -F '[error]' — continuously monitor the web.log file and output any new lines logged to it that match the fixed string [error]. More Info…
Doing the Downward Log
Hopefully the information presented here will give you some understanding of how the logging mechanism works, and what the data in the log files means.
And with understanding comes confidence
It’ll give you the flexibility and empowerment to do some error diagnosis on your own.
With this forensic information, we can have a pretty good idea what’s going on, and attempt to fix it, or at the very least have a really good bug report for Pixel & Tonic or a plugin author.
Even if you’re not ultimately the one who will fix the error, the better the bug report is that you pass along, the more likely it is that the issue will be resolved with alacrity.
If you want to flex your muscles even more, check out the Creating a Custom Logger for Craft CMS article.
Happy logging!
Further Reading
If you want to be notified about new articles, follow nystudio107 on Twitter.
Copyright ©2020 nystudio107. Designed by nystudio107
Top comments (0)