My logging system in Obsidian
Updated: 12/20/2022
tl;dr
Jump to the Final code
Jump to the updated version
Inspiration
I was watching the recordings from the Linking Your Thinking Conference and I got to Leah Ferguson: Start and End Your Day With a Daily Note. One takeaway I got from her talk was, "I too could use a low friction way of adding some logging in my vault!" I thought I would do just what she said but it quickly got out of hand.
The requirements
For this to work, you'll need some plugins:
- DataView
- Templater
- Sequence Hotkeys (optional)
The basic how-to
- In your daily note, you use a
DataView
field to log things, likelog-thingy::
Did the thingy!
In Leah's example, she's using log-podcast::
amongst others. I've got that and a few others like log-music, log-play, log-tv, and log-read.
- I used Templater to create some tiny templates for each of these so I don't have to type them perfectly each time.
- I use Sequence Hotkeys so I can have one main hotkey have multiple functions
Ctrl + L t
fires off thelog-tv::
templateCtrl + L m
fires off thelog-music::
template
Here's one day (out of 3) in the daily log:
Wherever you want a list of things you logged, you add a DataView query. Here's the basic idea from Leah altered for my vault. Let's do TV.
1TABLE log-tv As "TV Show"2FROM "10 Journal/11 Daily"3WHERE log-tv4FLATTEN file.link5SORT file.link desc
For each episode I watch, I make a quick note in my daily note log. Sometimes, I might just jot down a note. Sometimes, an episode, and if there is a note on the show, I'll put a link to it.
log-tv::
wasn't really watching but I know I was in the room for a few Guys Grocery Gameslog-tv::
Last Week Tonight with John Oliver - Inflationlog-tv::
[[) Good Girls]] - S4E15 - We're Even 1
Expanded use case
Now that I've started entering notes in the log, I want to see a log on the note itself! My initial idea was to add the note to the WHERE
clause in the query.2
1TABLE WITHOUT ID2 "📆 " + file.link AS "Date",3 log-tv4FROM "10 Journal/11 Daily"5WHERE contains(log-tv, "[[) Good Girls]]")6FLATTEN file.link7SORT file.link desc
Unfortunately, while this does give me only days where that show was watched, it includes all of the other log-tv entries as well.
I looked through the Dataview documentation and found the filter()
function. That'll do it!3
1TABLE WITHOUT ID2 "📆 " + file.link AS "Date",3 filter(log-tv, (x) => contains(x, "[[) Good Girls]]")) AS "TV Watched"4FROM "10 Journal/11 Daily"5WHERE contains(log-tv, "[[) Good Girls]]")6FLATTEN file.link7SORT file.link desc
Sure enough, that almost works!
At least now I'm starting to come up with some success criteria:
- ✅ Multiple logs for a day include all entries
- ❌ Single log for a day is included in results
A bit of console.log()-ing
and I find that if there are multiple logs for a day, they are stored in an array
(makes sense) but if there's a single log, it's just a string
. The filter()
function needs to work on arrays and the string just isn't included in the results magically.
At this point, I went off into DataView JavaScript land, sent everything into an endless loop, hard quit Obsidian, and went to bed.
DataView JS time
The next day, feeling refreshed, I remembered seeing a way to get a dataset in Dataview using normal Dataview syntax. I thought this would be best since I'm so close. It appears I can just plug my initial query into the dv.query()
function and get a nice result set. I removed the aliases and the filtering, thinking I'll do that in JavaScript.
1const data = await dv.query(`TABLE WITHOUT ID file.link, log-tv2 FROM "10 Journal/11 Daily"3 WHERE contains(log-tv, "[[) Good Girls]]")4 SORT file.link desc5 `)
My plan is to run through this data and create a new array wherein everything is an array so it looks consistent. My JavaScript skills are less than stellar but I sure thought I'd be doing something without forEach()
but here it is with forEach()
.
1if (data.successful) {2 let output = []3 data.value.values.forEach(function (e) {4 if (typeof e[1] === 'string') {5 output.push([e[0], Array(e[1])])6 } else {7 output.push([e[0], e[1].filter((e) => e.contains(`[[) Good Girls]]`))])8 }9 })10
11 dv.table(['Date', 'Episodes'], output)12}
Using console.log({data}) I could see there was a bit of nesting going on, hence data.value.values
is my starting point.
- "
values
is an array of stuff" is how I'm thinking of this. There are two possibilities for each row in the array:link, string
- When[0]
is a link and[1]
is a string, there is only one log entry for this type (i.e. log-tv) on the day and, because of the WHERE clause in the Dataview query, it is for the current show.- I thought at first I could just push this
string
into the new array but it has to be converted so it is an array itself. This was so much easier than I thought it would be. Array(string
) does the trick, at least here.
- I thought at first I could just push this
link, array
- When[1]
is not a string, it's an array of shows on that day and I want to filter that array so it only includes the current show- Again, because of the WHERE clause above, there's at least one record for this show for each day,
[0]
, so it's safe to push a new entry for this day. - Then push the current array but filter it for the current show.
- Again, because of the WHERE clause above, there's at least one record for this show for each day,
There's S4E15!
- ✅ Multiple logs for a day include all entries
- ✅ Single log for a day is included in results
Just in time!
It's working with JavaScript. For Good Girls, we're so close to the end of Season 4. We're going to watch the Season Finale tonight. I bet Season 5 will be very exciting. Wait, what? Cancelled? C'mon Netflix pick it up at least to give it some closure!
New requirements
Even when you're wasting time working on important projects for yourself, you run into things you know you'd like to change.
- I like the idea of having an emoji next to the date and I don't want to go through Supercharged Links to add it as I really don't want it everywhere. It seems easiest to do it in the query as I was doing that originally.
- I'm on the page, I don't need to see a link to the page!
- I'd done this before using
.replace()
- Since I'm basing lots of decisions on the current show being in the data, I wait until the end to fix this up
- I'd done this before using
1if (data.successful) {2 let output = []3 data.value.values.forEach(function (e) {4 if (typeof e[1] === 'string') {5 output.push([e[0], Array(e[1].replace('[[) Good Girls]] -', ''))])6 } else {7 output.push([8 e[0],9 e[1]10 .filter((e) => e.contains(`[[) Good Girls]]`))11 .map((e) => e.replace('[[) Good Girls]] -', '')),12 ])13 }14 })15
16 dv.table(['Date', 'Episodes'], output)17}
New problems
Yes! Done! Ready for Season 5...er, not done...
For my log-music
entries I have some that are just a link to the tune. Like this!
- log-music:: [[} Stanley Clarke - Silly Putty]]
What does this mean? By default, I suppose it means I listened to the tune. On this date, for this tune, I was getting ready to transcribe it. Find a recording, see if there's already a chart on the Internet4, and decide whether I need to transcribe it or not.
I went to the note in Obsidian for that tune and saw nothing! Nothing in the Weekly note showing all log-music entries either. Time for a couple more criteria for success!
- ✅ Multiple logs for a day include all entries
- ✅ Single log for a day is included in results
- ❌ A log entry that is just a link displays in results of log type (i.e. log-music)
- ❌ A log entry that is just a link displays in results for the linked page
A few more console.log()'s later and...
- When there is a single value, if it's a string, it needs to convert to an array.
- If it is only a link5, then it also needs to convert to an array. In this case, it is a
Link
object and it has apath
property.
This will fix it for my weekly view and should work anytime you want to display a list of all of a log type (i.e. log-music).
1data.value.values.forEach(function (e) {2 if (typeof e[1] === 'string') {3 output.push([e[0], Array(e[1])])4 } else if (typeof e[1] === 'object' && e[1].hasOwnProperty('path')) {5 output.push([e[0], Array(e[1])])6 } else {7 output.push([e[0], e[1]])8 }9})
- ✅ Multiple logs for a day include all entries
- ✅ Single log for a day is included in results
- ✅ A log entry that is just a link displays in results of log type (i.e. log-music)
- ❌ A log entry that is just a link displays in results for the linked page
The final problem
Now, I'm looking at having a log on the note page that shows I did do something related to this note on a day but I wrote absolutely nothing else about it.
Options
- Have some sort of default text in the note. But what?
- Have nothing in the note other than a link to the daily note, rather like a back link. But we already have back links.
- Always include something in a log entry. It could be quick! For music it could be listened or practiced.
Currently, I've left things so if the log is just a link, it will not display in the linked note. I think I'd rather force myself to add a word or two rather than just put a link and be done with it.
Final code
Here are the templates I came up with for my Weekly and Individual Notes. Place these inside of dataviewjs
code fences.
Temporal query
- Change all occurrences of
log-tv
to whatever your log field is called. - This is in my weekly note. You can use whatever you need for the date range. Perhaps this goes into your monthly note? Perhaps remove it to show all entries?
- Change the header, Watched, to something that makes sense for what you are logging.
1const data = await dv.query(`TABLE WITHOUT ID "📆 " + file.link, log-tv2 FROM "10 Journal/11 Daily"3 WHERE log-tv4 AND file.ctime >= date(<% tp.date.weekday("gggg-MM-DD", 1, tp.file.title, "gggg-[W]ww") %>)5 AND file.ctime < date(<% tp.date.weekday("YYYY-MM-DD", 8, tp.file.title, "gggg-[W]ww") %>)6 SORT file.link desc7 `)8
9if (data.successful) {10 let output = []11 data.value.values.forEach(function (e) {12 if (typeof e[1] === 'string') {13 output.push([e[0], Array(e[1])])14 } else if (typeof e[1] === 'object' && e[1].hasOwnProperty('path')) {15 output.push([e[0], Array(e[1])])16 } else {17 output.push([e[0], e[1]])18 }19 })20
21 dv.table(['Date', 'Watched'], output)22}
Note query
- Change all occurrences of
log-tv
to whatever your log field is called. - Change the header, Episodes, to something that makes sense for your log field.
1const data = await dv.query(`TABLE WITHOUT ID "📆 " + file.link, log-tv2 FROM "10 Journal/11 Daily"3 WHERE contains(log-tv, "[[<%tp.file.title%>]]")4 SORT file.link desc5 `)6
7if (data.successful) {8 let output = []9 data.value.values.forEach(function (e) {10 if (typeof e[1] === 'string') {11 output.push([e[0], Array(e[1].replace('[[<%tp.file.title%>]] -', ''))])12 } else if (typeof e[1] === 'object' && e[1].hasOwnProperty('path')) {13 output.push([e[0], Array(e[1])])14 } else {15 output.push([16 e[0],17 e[1]18 .filter((e) => e.contains(`[[<%tp.file.title%>]]`))19 .map((e) => e.replace('[[<%tp.file.title%>]] -', '')),20 ])21 }22 })23
24 dv.table(['Date', 'Episodes'], output)25}
Refactored note query
As I started using this a lot, I noticed if I ever wanted to change the title of the note, I had to go and manually fix the title in the query. This smelled funny to me.
I realized I was being very Obsidian-centric when this is just JavaScript code. I should have refactored to remove duplication of <%tp.file.title%>
everywhere.
I introduced a new variable for the note title.
1const noteTitle = dv.current().file.name2const data = await dv.query(`TABLE WITHOUT ID "📆 " + file.link, log-tv3 FROM "10 Journal/11 Daily"4 WHERE contains(log-tv, "[[<%tp.file.title%>]]")5 SORT file.link desc6 `)
Then I replaced all the hard coded spots with noteTitle
using string interpolation.
1const noteTitle = dv.current().file.name2const data = await dv.query(`TABLE WITHOUT ID "📆 " + file.link, log-tv3 FROM "10 Journal/11 Daily"4 WHERE contains(log-tv, "[[${noteTitle}]]")5 SORT file.link desc6 `)7
8let output = []9data.value.values.forEach(function (e) {10 if (typeof e[1] === 'string') {11 output.push([e[0], Array(e[1].replace(`[[${noteTitle}]] -`, ''))])12 } else if (typeof e[1] === 'object' && e[1].hasOwnProperty('path')) {13 output.push([e[0], Array(e[1])])14 } else {15 output.push([16 e[0],17 e[1]18 .filter((e) => e.contains(`[[${noteTitle}]]`))19 .map((e) => e.replace(`[[${noteTitle}]] -`, '')),20 ])21 }22})23
24if (data.successful) {25 dv.table(['Date', 'Episodes'], output)26}
Now I can change titles without having to manually update this query.
Have fun!
Attributions
Photo by Oliver Paaske (@photolli) on Unsplash
- I'm using codes for media recommended by Bryan Jenks in his My 2021 Comprehensive Obsidian Zettelkasten Workflow video. The
)
indicates it is Entertainment. It's long but I think I watched the whole thing when I was getting started with Obsidian!↩ - I also added a little emoji in there for grins.↩
- And since it uses an arrow function, it's cool! Luckily I know enough JavaScript to get that. Not much more mind you!↩
- There was but too many pages. I only needed the keyboard part!↩
- I should probably never say only a link in the context of Obsidian but, there ya go!↩