2024
Activism⚑
-
New: Introduction to activism.
Activism consists of efforts to promote, impede, direct or intervene in social, political, economic or environmental reform with the desire to make changes in society toward a perceived greater good.
-
New: Recomendar el episodio de podcast el diario de Jadiya.
Diario de Jadiya (link al archivo): es algo que todo xenófobo debería de escuchar, es un diario de una chavala saharaui que formó parte del programa de veranos en familias españolas.
-
New: Añadir el vídeo del racismo no se sostiene.
-
New: Nuevo artículo contra el turismo.
- Abolir el turismo - Escuela de las periferias: Lleguemos a donde lleguemos, no puede ser que sea más fácil imaginar el fin del capitalismo que el fin del turismo.
Antifascism⚑
Hacktivism⚑
Chaos Communication Congress⚑
-
New: Introduce the CCC.
Chaos Communication Congress is the best gathering of hacktivism in europe.
Prepare yourself for the congress
You can follow MacLemon's checklist
The schedule app
You can use either the Fahrplan app or giggity, I've been using the second for a while, so is the one I use
The navigation app
c3nav
is an application to get around the congress. The official F-droid is outdated, so add their repository to get the latest version.Reference - [Home](https://events.ccc.de/en/ - Engelsystem - Angel FAQ
-
New: Introduce the Angel's system.
Angels are participants who volunteer to make the event happen. They are neither getting paid for their work nor do they get free admission.
Helping at our events also comes with some simple, but important expectations of you:
- Be on time for your shift or give Heaven early notice.
- Be well rested, sober and not hungry.
- Be open-minded and friendly in attitude.
- Live our moral values:
- Be excellent to each other.
- All creatures are welcome.
- Create yourself an Engelsystem account
- Arrive at the venue
- Find the Heaven and go there.
- Talk to a welcome angel or a shift coordinator to get your angel badge and get marked as arrived.
- If you have any questions, you can always ask the shift coordinators behind the counter.
- Attend an angel meeting
- Announced in the Engelsystem news
- Click yourself an interesting shift
- Read shift descriptions first
- Participate in your shift
- Use the navigation to find the right place.
- Arrive a little bit early at the meeting point
- Rest for at least one hour
- Repeat from step 5
And always, have a lot of fun.
To get more insights read this article
The Engelsystem is the central place to distribute the work to all the helping angels. It can be a bit overwhelming at the beginning but you will get used to it and find your way around.
As you might have seen there are many different shifts and roles for angels — some sounding more appealing than others. There are shifts where you need to have some knowledge before you can take them. This knowledge is given in introduction meetings or by taking an unrestricted shift in the team and getting trained on the job. These introduction meetings are announced in the Engelsystem under the tab "Meetings". Heaven and the teams try to make sure that there are only restrictions for shifts in place where they are absolutely needed.
Most restrictions really only need a meeting or some unrestricted shifts at the team to get lifted. Harder restrictions are in place where volunteers need to have special certification, get access to certain systems with a huge amount of data (e.g. mail-queues with emails from participants) or handling big piles of money. Usually the requirements for joining an angeltype are included in the description of the angeltype.
Especially the restricted shifts are tempting because after all we want to get the event running, aren’t we? From our personal experience what gets the event running are the most common things: Guarding a door, collecting bottle/trash, washing dishes in the angel kitchen, being on standby to hop in when spontaneous help is needed or check the wrist band at the entrance.
If there are any further questions about angeltypes, the description of the angeltype usually includes contact data such as a DECT number or an e-mail address that can be used. Alternatively, you can also ask one of the persons of the respective angeltype mentioned under "Supporter".
Congress is organized from different teams, each with its own area of expertise.
All teams are self-organized and provide their own set of services to the event.
Teams spawn into existence by a need not fulfilled. They are seldom created by an authority.
Check out the different teams to see which one suits you best.
Some poeple suggest not to try to fit into special roles at your first event. The roles will find you – not the other way around. Our community is not about personal growing but about contributing to each other and growing by doing this.
Perks
Being an angel also comes with some perks. While we hope that participation is reward enough, here is a list of things that are exclusive to angels:
- Community acknowledgement
- Hanging out in Heaven and the angel hack center with its chill out area
- Free coffee and (sparkling) water
- Warm drinks or similar to make the cold night shifts more bearable
Rewards
If you have contributed a certain amount of time, you may receive access to:
- Fantastic hot vegan and vegetarian meals
- The famous limited™ angel T-shirt in Congress design
- Maybe some other perks
Feminism⚑
Privileges⚑
Free Knowledge⚑
-
Correction: Update the way of seeding ill knowledge torrents.
A good way to contribute is by seeding the ill torrents. You can generate a list of torrents that need seeding up to a limit in TB. If you follow this path, take care of IP leaking, they're
Free Software⚑
-
New: Recomendar el artículo El software libre también necesita jardineros.
Conference organisation⚑
-
New: Software to manage the conference.
There are some open source software that can make your life easier when hosting a conference:
In addition to the management of talks from the call for papers till the event itself it can help the users visualise the talks schedule with EventFahrplan which is what's used in the ChaosComputerClub congress.
If you also want to coordinate helpers and shifts take a look to Engelsystem
Ludditest⚑
-
New: Nice comic about the luddites.
Life Management⚑
Time management⚑
-
New: Anticapitalist approach to time management.
Time management is being used to perpetrate the now hegemonic capitalist values. Its a pity because the underlying concepts are pretty useful and interesting but they are oriented towards improving productivity and being able to deal with an increasing amount of work. Basically they're always telling you to be a better cog. It doesn't matter how good you are, there is always room for improvement. I've fallen on this trap for a long time (I'm still getting my head out of the hole) and I'm trying to amend things by applying the concepts on an anticapitalist mindset. The turning point was to read Four thousand weeks: Time management for mortals by Oliver Burkeman, the article will have book extracts mixed with my way of thinking.
Some (or most) of what's written in this article may not apply if you're not a male, white, young, cis, hetero, European, university graduated, able-bodied, """wealthy""" person. You need to be at a certain level of the social ladder to even start thinking in these terms. And depending on the number of oppressions you're suffering you'll have more or less room to maneuver. That margin is completely outside our control so by no means we should feel guilty of not being able to manage time. What follows are just guidelines to deal with this time anxiety imposed by the capitalist system with whatever air we have to breath.
Changing the language
The easiest way to change the underlying meaning is to change the language. Some substitutions are:
work
->focus
: Nowadays we usework
everywhere even if it's not in the laboral environment. For example I work very hard to achieve my goals. Working is the action of selling your time and energies in order to get the resources you need to live. It has an intrinsic meaning of sacrifice, doing something we don't want to do to get another thing in return. That's a tainted way of thinking about your personal time. I findfocus
is a great substitute as it doesn't have all those connotations. There are similar substitutions based on the same argument, such as:workspace
->space of focus
,workflow
->action flow
or justflow
.task
->action
: Similar towork
atask
is something you kind of feel obliged to do. It uses a negative mindset to set the perfect scenario to feel guilty when you fail to do them. But you're on your personal time, it should be fine not to do an action for whatever reason.Action
on the other side fosters a positive way of thinking, it suggests change, movement in a way that helps you move forward. There are also other derived words such astask manager
->action manager
.productivity
->efficiency
:Productivy
is the measurement of how fast or good you create products. And products are something that is made to be sold. Again this introduces a monetary mindset on all aspects of our life.Efficiency
on the other side is the quality of achieving the largest amount of change using as little time, energy or effort as possible (Cambridge doesn't agree with me though :P. It may be because universities are also another important vector of spreading the capitalist values:(
). So using efficiency we're focusing more on improving the process itself, so it can be applied for example on how to optimize your enjoyment of doing nothing. Which is completely antagonistic to the concept of productivity.
Changing the mindset
There is a widespread feeling that we're always short on time. We're obsessed with our overfilled inboxes and lengthening todo lists, haunted by the guilty feeling that we ought to be getting more done, or different things done, or both. At the same time we're deluged with advice on living the fully optimized life to squeeze the most from your time. And it get's worse as you age because time seems to speed up as you get older, steadily accelerating until months begging to flash by in what feels like minutes.
The real problem isn't our limited time. It's that we've unwittingly inherited, and feel pressured to live by a troublesome set of ideas about how to use our limited time, all of which are pretty much guaranteed to make things worse. What follows are a list of mindset changes from the traditional time management bibliography that can set the bases of a healthier Anticapitalist one.
Time is not a resource to spend
Before timetables life rhythms emerged organically from the tasks they needed to do. You milked the cows when they needed milking and harvested the crops when it was harvest time. Anyone who would tried imposing an external schedule on any of that, for example, doing a month's milking in a single day to get it out of the way would rightly have been considered a lunatic.
There was no need to think of time as something abstract and separate from life. In those days before clocks, when you did need to explain how long something might take, your only option was to compare it with some other concrete activity. They were untroubled by any notion of time "ticking away" thus living a heightened awareness of the vividness of things, the feeling of timelesness. Also known as living in deep time, or being in the flow, when the boundary separating the self from the rest of reality grows blurry and time stands still.
There's one huge drawback in giving so little thought to the abstract idea of time, though, which is that it severely limits what you can accomplish. As soon as you want to coordinate the actions of more than a handful of people, you need a reliable, agreed-upon method of measuring time. This is why the first mechanical clocks came to be invented.
Making time standardized and visible in this fashion inevitably encourages people to think of it as an abstract thing with an independent existence, distinct from the specific activities on which one might spend it. "time" is what ticks away as the hands move around the clock face.
The next step was to start treating time as a resource, something to be bought and sold and used as efficiently as possible. This mindset shift serves as the precondition for all the uniquely modern ways in which we struggle with time today. Once time is a resource to be used, you start to feel pressure, whether from external forces or from yourself, to use it well, ant to berate yourself when you feel you've wasted it. When you're faced with too many demands, it's easy to assume that the only answer must be to make better use of time, by becoming more efficient, driving yourself harder, or working longer instead of asking whether the demands themselves might be unreasonable.
Soon your sense of self-worth gets completely bound up with how you're using time: it stops being merely the water in which you swim and turns into something you fell you need to dominate or control if you're to avoid feeling guilty, panicked or overwhelmed.
The fundamental problem is that this attitude towards time sets up a rigged game in which it's impossible ever to feel as though you're doing well enough. Instead of simply living our lives as they unfold in time it becomes difficult not to value each moment primarily according to its usefulness for some future goal, or for some future oasis of relaxation you hope to reach once your tasks are finally "out of the way".
Ultimately it backfires. It wrenches us out of the present, leading to a life spent leaning into the future, worrying about whether things will work out, experiencing everything in terms of some later, hoped-for benefit, so that peace of mind never quite arrives. And it makes it all but impossible to experience the flow, that sense of timeless time which depends on forgetting the abstract yardstick and plunging back into the vividness of reality instead.
If you don't disavow capitalism an increase in efficiency will only make things worse
All this context makes us eager to believe the promises of time management frameworks (like GTD) that if you improve your efficiency you'll get more time to enjoy your life. If you follow the right time management system, build the right habits, and apply sufficient self-discipline, you will win the struggle with time.
Reality then kicks in you never win the struggle and only feel more stressed and unhappy. You realize that all the time you've saved is automatically filled up by more things to do in a never ending feedback loop. It's true that you get more done, and yet, paradoxically, you only feel busier, more anxious and somehow emptier as a result. Time feels like an unstoppable conveyor belt, bringing us new actions as fast as we can dispatch the old ones; and becoming more efficient just seems to cause the belt to speed up. Or else, eventually, to break down.
It also has another side-effect. As life accelerates, everyone grows more impatient. It's somehow vastly more aggravating to wait two minutes for the microwave than two hours for the oven, or ten seconds for a slow loading web page versus three days to receive the same information by mail.
Denying reality never works though. It may provide some immediate relief, because it allows you to go on thinking that at some point in the future you might, at last, feel totally in control. But it can't ever bring the sense that you're doing enough (that you are enough) because it defines enough as a kind of limitless control that no human can attain. Instead, the endless struggle leads to more anxiety and less fulfilling life. For example, the more you believe yo might succeed in "fitting everything in", the more commitments you naturally take on, and the less you feel the need to ask whether each new commitment is truly worth a portion of your time, and so your days inevitably fill with more activities you don't especially value. The more you hurry, the more frustrating it is to encounter tasks that won't be hurried, the more compulsively you plan for the future, the more anxious you feel about any remaining uncertainties, of which there will always be plenty.
Time management used this way serves as a distraction to numb our minds:
- It may hide the sense of precariousness inherent to the capitalist world we live in. If you could meet every boss's demand, while launching various side projects on your own, maybe one day You'd finally feel secure in your career and your finances.
- Divert your energies from fully experiencing the reality in which you find yourself, holding at bay certain scary questions about what you're doing with your life, and whether major changes might not be needed. As long as you're always just on the cusp of mastering time, you can avoid the thought that what life is really demanding from you might involve surrendering the craving for mastery and diving into the unknown instead.
Embrace the finitude of time
We recoil from the notion that this is it. That this life, with all its flaws and inescapable vulnerabilities, its extreme brevity, and our limited influence over how it unfolds, is the only one we'll get a shot at. Instead, we mentally fight against the way things are, so that we don't have to consciously participate in what it's like to feel claustrophobic, imprisoned, powerless, and constrained by reality.
Our troubled relationship with time arises largely from this same effort to avoid the painful constrains of reality. And most of our strategies for becoming more efficient make things worse, because they're really just ways of furthering the avoidance. After all, it's painful to confront how limited your time is, because it means that tough choices are inevitable and that you won't have time for all you once dreamed you might do. It's also painful to accept the limited control over the time you do get: maybe you simply lack the stamina or talent or other resources to perform well in all the roles you feel you should. And so, rather than face our limitations, we engage in avoidance strategies, in an effort to carry on feeling limitless. We push ourselves harder, chasing fantasies of the perfect work-life balance, or we implement time management systems that promise to make time for everything, so that tough choices won't be required. Or we procrastinate, which is another means of maintaining the feeling of omnipotent control over life, because you needn't risk the upsetting experience of failing at an intimidating project if you never even start it. We fill our minds with busyness and distraction to numb ourselves emotionally. Or we plan compulsively, because the alternative is to confront how little control over the future we really have.
Heal yourself from FOMO
In practical terms, a limit-embracing attitude to time means organizing your days with the understanding that you definitely won't have time for everything you want to do, or that other people want you to do, and so, at the very least, you can stop beating yourself up for failing. Since hard choices are unavoidable, what matters is learning to make them consciously, deciding what to focus on and what to neglect, rather than letting them get made by default, or deceiving yourself that, with enough hard work and the right time management tricks, you might not have to make them at all. It also means resisting the temptation to "keep your options open" in favour of deliberately making big, daunting, irreversible commitments, which you can't know in advance will turn out for the best, but which reliably prove more fulfilling in the end. And it means standing firm in the face of FOMO (fear of missing out) because you come to realize that missing out on something (indeed on almost everything) is basically guaranteed. Which isn't actually a problem anyway, it turns to, because "missing out" is what makes your choices meaningful in the first place. Every decision to use a portion of time on anything represents the sacrifice of all the other ways in which you could have spent that time, but didn't, and to willingly make that sacrifice is to take a stand, without reservation, on what matters most to you.
Embrace your control limits
The more you try to manage your time with the goal of achieving a feeling of total control and freedom from the inevitable constrains of being human, the more stressful, empty, and frustrating life gets. But the more you confront the facts of finitude instead, and work with them, rather than against them, the more efficient, meaningful and joyful life becomes. Anxiety won't ever completely go away, we're even limited, apparently, in our capacity to embrace our limitations. But I'm aware of no other time management technique that's half as effective as just facing the way things truly are.
Time pressure comes largely from forces outside our control: from a cutthroat economy; from the loss of the social safety networks that used to help ease the burdens of work and childcare; and from the sexist expectation that women must excel in their careers while assuming most of the responsibilities at home. None of that will be solved with time management. Fully facing the reality of it can only help though. So long as you continue to respond to impossible demands on your time by trying to persuade yourself that you might one day find some way to do the impossible, you're implicitly collaboration with those demands. Whereas once you deeply grasp that they are impossible, you'll stop believing the delusion that any of that is ever going to bring satisfaction and will be newly empowered to resist them, letting you focus instead on building the most meaningful life you can, in whatever situation you're in.
Seeing and accepting our limited powers over our time can prompt us to question the very idea that time is something you use in the first place. There is an alternative: the notion of letting time use you, approaching life not as an opportunity to implement your predetermined plans for success but as a matter of responding to the needs of your place and your moment in history.
Embrace the community constrains
Moreover, most of us seek a specifically individualistic kind of mastery over time. Our culture's ideal is that you alone should control your schedule, doing whatever you prefer, whenever you want, because it's scary to confront the truth that almost everything worth doing depends on cooperating with others, and therefore on exposing yourself to the emotional uncertainties of relationships. In the end the more individual sovereignty you achieve over your time, the lonelier you get. The truth then is that freedom sometimes is to be found not in achieving greater sovereignty over your own schedule but in allowing yourself to be constrained by the rhythms of community. Participating in forms of social life where you don't get to decide exactly what you do or when you doi it. And it leads to the insight that meaningful efficiency often comes not from hurrying things up but from letting them take the time they take.
Live for today not tomorrow
It doesn't matter what you do, we all sense that there are always more important and fulfilling ways we could be spending our time, even if we can't say exactly what they are, yet we systematically spend our days doing other things instead. This feeling can take many forms: the desire to devote yourself to some larger cause, continuously demanding more from yourself, desiring to spend more time with your loved ones.
Our attempts to become more efficient may have the effect of pushing the genuinely important stuff even further over the horizon. Our days are spent trying to "get through" tasks, in order to get them "out of the way", with the result that we live mentally in the future, waiting for when we'll finally get around to what really matters, and worrying in the meantime, that we don't measure up, that we might lack the drive or stamina to keep pace with the speed at which life now seems to move. We live in a constant spirit of joyless urgency.
-
New: Time is not a resource to be tamed.
You'll see everywhere the concept of
time management
. I feel it's daring to suggest that you have the power to actually manage time. No you can't as much as you can't tame the sea. Time is not a resource to be spent or be managed, the best we can do is to try to understand its flows and navigate it the best we can. -
New: Keep on summing up Oliver Burkeman book.
Efficiency doesn't necessarily give you more time
We're eager to believe the promises of time management frameworks (like GTD) that if you improve your efficiency you'll get more time to enjoy your life. If you follow the right time management system, build the right habits, and apply sufficient self-discipline, you will win the struggle with time.
Reality then kicks in you never win the struggle and only feel more stressed and unhappy. You realize that all the time you've saved is automatically filled up by more things to do in a never ending feedback loop. Time feels like an unstoppable conveyor belt, bringing us new actions as fast as we can dispatch the old ones; and becoming more efficient just seems to cause the belt to speed up. Or else, eventually, to break down. It's true that you get more done, and yet, paradoxically, you only feel busier, more anxious and somehow emptier as a result.
It get's even worse because importance is relative and you may fall into efficiency traps.
Heal yourself from FOMO
Another problem that FOMO brings us is that it leads us to lives where you "truly lived" only if you've lived all the experiences you could live. This leads to a frustrating life as the world has infinite of them, so getting a handful of them under your belt brings you no closer to a sense of having feasted on life's possibilities. You lead yourself in another efficiency trap where the more you experience the more additional wonderful experiences you sarta to feel you could have on top of all those you've already had, with the result that the feeling of existential overwhelm gets worse. To fight this existential overwhelm you can resist the urge to consume more and more experiences and embrace the idea that you're going to miss most of them. You'll then be able to focus on fully enjoying the tiny slice of experiences you actually do have time for.
This FOMO fever is normal given the facts that we're more conscious of the limits of our time (after deterring the after life), the increase of choices that the world has brought us, and the internet amplifier.
You do what you can do
It's usual to feel as though you absolutely must do more than you can do. We live overwhelmed in a constant anxiety of fearing, or knowing for certain, that the actions we want to carry out won't fit on our available time. It looks like this feeling arises on every step of the economic ladder (shown in the works of Daniel Markovits).
The thing is that the idea in itself doesn't make any sense. You can't do more than you can do even if you must. If you truly don't have time for everything you want to do, or feel you ought to do, or that others are badgering you to do, then, well, you don't have time, no matter how grave the consequences of failing to do it all might prove to be. So technically it's irrational to feel troubled by an overwhelming to-do list. You'll do what you can, you won't do what you can't, and the tyrannical inner voice insisting that you must do everything is simply mistaken. We rarely stop to consider things so rationally, though, because that would mean confronting the painful truth of our limitations. We would be forced to acknowledge that there are hard choices to be made: which balls to let drop, which people to disappoint, which ambitions to abandon, which roles to fail at... Instead, in an attempt to avoid these unpleasant truths, we deploy the strategy that dominates most conventional advice on how to deal with busyness: we tell ourselves we'll just have to find a way to do more. So to address our busyness we're making ourselves busier still.
Importance is relative
The problem here is that you'll never be able to make time for everything that feels important. A similar mindset of the section Efficiency doesn't give you more time can be applied. The reason isn't that you haven't yet discovered the right time management tricks, or applied sufficient effort, or that you're generally useless. It's that the underlying assumption is unwarranted: there's no reason to believe you'll make time for everything that matters simply by getting more done. For a start, what "matters" is subjective, so you've no grounds for assuming that there will be time for everything that you, or anyone else deems important. But the other exasperating issue is that if you succeed in fitting more in, you'll find the goalposts start to shift: more things will begin to seem important, meaningful or obligatory. Acquire a reputation for doing your work at amazing speed, and you'll be given more of it. An example of this is gathered in Ruth Schwartz's book More work for mother, where it shows that when washing machines and vacuum cleaners appeared no time was saved at all, because society's standards of cleanliness rose to offset the benefits. What needs doing expands so as to fill the time available for its completion.
Be mindful of the efficiency trap
Sometimes improving your efficiency may lead you to a worse scenario ("efficiency trap") where you won't generally result in the feeling of having "enough time", because, all else being equal, the demands will increase to offset any benefits. Far from getting things done, you'll be creating new things to do. A clear example of this is email management. Every time you reply to an email, there's a good chance of provoking a reply to that email, which itself may require another reply, and so on and so on. At the same time, you'll become known as someone who responds promptly to email, so more people will consider it worth their while to message you to begin with. So it's not simply that you never get though your email; it's that the process of "getting through your email" actually generates more email.
For most of us, most of the time, it isn't feasible to avoid the efficiency trap altogether, but you can stop believing you'll ever solve the challenge of busyness by cramming more in, because that just makes matters worse. And once you stop investing in the idea that you might one day achieve peace of mind that way, it becomes easier to find peace of mind in the present, in the midst of overwhelming demands, because you're no longer making your peace of mind dependent on dealing with all the demands. Once you stop believing that it might somehow be possible to avoid hard choices about time, it gets easier to make better ones.
If you also have the knowledge of the existence of the efficiency traps you may detect them and try to get the benefits without the penalties.
Do the important stuff
The worst aspect of the trap is that it's also a matter of quality. The harder you struggle to fit everything in, the more of your time you'll find yourself spending on the least meaningful things. This is because the more firmly you believe it ought to be possible to find time for everything, the less pressure you'll feel to ask whether any given activity sis the best use of a portion of your time. Each time something new shows up, you'll be strongly biased in favor of accepting it, because you'll assume you needn't sacrifice any other tasks or opportunities in order to make space for it. Soon your life will be automatically filled with not just more things but with more trivial or tedious things.
The important stuff gets postponed because such tasks need your full focus, which means to wait until you have a good chunk of free time and fewer small-but-urgent tasks tugging at your attention. So you spend your energy into clearing the decks, cranking through the smaller stuff to get it out of the way, only to discover that doing so takes the whole day, that the decks are filled up again overnight and that the moment for doing the important stuff never arrives. One can waste years this way, systematically postponing precisely the things one cares the most.
What's needed in these situations is to resist the urges of being on top of everything and learn to live with the anxiety of feeling overwhelmed without automatically responding by trying to fit more in. Instead of clearing the decks, decline to do so, focusing instead on what's truly of greatest consequence while tolerating the discomfort of knowing that, as you do so, the decks will be filling up further, with emails and errands and other to-dos, many of which you may never get around to at all.
You'll sometimes still decide to drive yourself hard in an effort to squeeze more in, when circumstances absolutely require it. But that won't be your default mode, because you'll no longer be operating under the illusion of one day making time for everything.
Evaluate what you miss when you increase your efficiency
Part of the benefits of efficiency is that you free yourself from tedious experiences, the side effect is that some times we're not conscious of being removing experiences that we valued. So even if everything runs more smoothly, smoothness is a dubious virtue, since it's often the unsmoothed textures of life that makes them livable, helping nurture the relationships that are crucial for mental and physical health, and for the resilience of our communities. For example if you buy online the groceries you miss the chance to regularly meet with your neighbours at your local grocery store.
Convenience makes things easy, but without regard to whether easiness is truly what's most valuable in any given context. When you render the process more convenient you drain it of its meaning. The effect of convenience isn't just that the given activity starts to feel less valuable, but that we stop engaging in certain valuable activities altogether, in favour of more convenient ones. Because you can stay home, order food online, and watch sitcoms on a streaming service, you find yourself doing so although you might be perfectly aware that you'd have had a better time if you had met with your friends.
Meanwhile, those aspects of life that resist being made to run more smoothly start to seem repellent. When you can skip the line and buy concert tickets on your phone, waiting in line to vote in an election is irritating. As convenience colonizes everyday life, activities gradually sort themselves into two types: the kind that are now far more convenient, but that feel empty or out of sync with our true preferences; and the kind that now seem intensely annoying because of how inconvenient they remain. Resisting all this is difficult because the Capital is winning this discourse and you'll have more pressure from your environment to stay convenient.
vdirsyncer⚑
-
New: Troubleshoot Database is locked.
First try to kill all stray vdirsyncer processes, if that doesn't work check for more solutions in this issue
-
```ini [pair calendar_name] a = "calendar_name_local" b = "calendar_name_remote" collections = null conflict_resolution = ["command", "vimdiff"] metadata = ["displayname", "color"]
[storage calendar_name_local] type = "filesystem" path = "~/.calendars/calendar_name" fileext = ".ics"
[storage calendar_name_remote] type = "http" url = "https://example.org/calendar.ics"
-
New: Automatically sync calendars.
You can use the script shown in the automatically sync emails
Org Mode⚑
-
New: Start working on a task dates.
SCHEDULED
defines when you are plan to start working on that task.The headline is listed under the given date. In addition, a reminder that the scheduled date has passed is present in the compilation for today, until the entry is marked as done or disabled.
*** TODO Call Trillian for a date on New Years Eve. SCHEDULED: <2004-12-25 Sat>
Although is not a good idea (as it promotes the can pushing through the street), if you want to delay the display of this task in the agenda, use
SCHEDULED: <2004-12-25 Sat -2d>
the task is still scheduled on the 25th but will appear two days later. In case the task contains a repeater, the delay is considered to affect all occurrences; if you want the delay to only affect the first scheduled occurrence of the task, use--2d
instead.Scheduling an item in Org mode should not be understood in the same way that we understand scheduling a meeting. Setting a date for a meeting is just a simple appointment, you should mark this entry with a simple plain timestamp, to get this item shown on the date where it applies. This is a frequent misunderstanding by Org users. In Org mode, scheduling means setting a date when you want to start working on an action item.
You can set it with
<leader>s
(Default:<leader>ois
) -
New: Deadlines.
DEADLINE
are like appointments in the sense that it defines when the task is supposed to be finished on. On the deadline date, the task is listed in the agenda. The difference with appointments is that you also see the task in your agenda if it is overdue and you can set a warning about the approaching deadline, startingorg_deadline_warning_days
before the due date (14 by default). It's useful then to setDEADLINE
for those tasks that you don't want to miss that the deadline is over.An example:
* TODO Do this DEADLINE: <2023-02-24 Fri>
You can set it with
<leader>d
(Default:<leader>oid
).If you need a different warning period for a special task, you can specify it. For example setting a warning period of 5 days
DEADLINE: <2004-02-29 Sun -5d>
.If you're as me, you may want to remove the warning feature of
DEADLINES
to be able to keep your agenda clean. Most of times you are able to finish the task in the day, and for those that you can't specify aSCHEDULED
date. To do so set the default number of days to0
.require('orgmode').setup({ org_deadline_warning_days = 0, })
Using too many tasks with a
DEADLINE
will clutter your agenda. Use it only for the actions that you need to have a reminder, instead try to using appointment dates instead. The problem of using appointments is that once the date is over you don't get a reminder in the agenda that it's overdue, if you need this, useDEADLINE
instead. -
New: Introduce org-rw.
org-rw
is a Python library to process your orgmode files.Installation:
pip install org-rw
Load an orgmode file:
from org_rw import load with open('your_file.org', 'r') as f: doc = load(f)
-
return { 'nvim-orgmode/orgmode', ```lua { 'nvim-orgmode/orgmode', dependencies = { { 'nvim-treesitter/nvim-treesitter', lazy = true }, }, event = 'VeryLazy', config = function() -- Load treesitter grammar for org require('orgmode').setup_ts_grammar() -- Setup treesitter require('nvim-treesitter.configs').setup({ highlight = { enable = true, additional_vim_regex_highlighting = { 'org' }, }, ensure_installed = { 'org' }, }) -- Setup orgmode require('orgmode').setup({ org_agenda_files = '~/orgfiles/**/*', org_default_notes_file = '~/orgfiles/refile.org', }) end, } ``` dependencies = { { 'nvim-treesitter/nvim-treesitter', lazy = true }, }, event = 'VeryLazy', config = function() -- Load treesitter grammar for org require('orgmode').setup_ts_grammar() -- Setup treesitter require('nvim-treesitter.configs').setup({ highlight = { enable = true, additional_vim_regex_highlighting = { 'org' }, }, ensure_installed = { 'org' }, }) -- Setup orgmode require('orgmode').setup({ org_agenda_files = '~/orgfiles/**/*', org_default_notes_file = '~/orgfiles/refile.org', }) end, }
-
New: Troubleshoot orgmode with dap.
Use the next config and follow the steps of Create an issue in the orgmode repository.
vim.cmd([[set runtimepath=$VIMRUNTIME]]) vim.cmd([[set packpath=/tmp/nvim/site]]) local package_root = '/tmp/nvim/site/pack' local install_path = package_root .. '/packer/start/packer.nvim' local function load_plugins() require('packer').startup({ { 'wbthomason/packer.nvim', { 'nvim-treesitter/nvim-treesitter' }, { 'nvim-lua/plenary.nvim'}, { 'nvim-orgmode/orgmode'}, { 'nvim-telescope/telescope.nvim'}, { 'lyz-code/telescope-orgmode.nvim' }, { 'jbyuki/one-small-step-for-vimkind' }, { 'mfussenegger/nvim-dap' }, { 'kristijanhusak/orgmode.nvim', branch = 'master' }, }, config = { package_root = package_root, compile_path = install_path .. '/plugin/packer_compiled.lua', }, }) end _G.load_config = function() require('orgmode').setup_ts_grammar() require('nvim-treesitter.configs').setup({ highlight = { enable = true, additional_vim_regex_highlighting = { 'org' }, }, }) vim.cmd([[packadd nvim-treesitter]]) vim.cmd([[runtime plugin/nvim-treesitter.lua]]) vim.cmd([[TSUpdateSync org]]) -- Close packer after install if vim.bo.filetype == 'packer' then vim.api.nvim_win_close(0, true) end require('orgmode').setup({ org_agenda_files = { './*' } } ) -- Reload current file if it's org file to reload tree-sitter if vim.bo.filetype == 'org' then vim.cmd([[edit!]]) end end if vim.fn.isdirectory(install_path) == 0 then vim.fn.system({ 'git', 'clone', 'https://github.com/wbthomason/packer.nvim', install_path }) load_plugins() require('packer').sync() vim.cmd([[autocmd User PackerCompileDone ++once lua load_config()]]) else load_plugins() load_config() end require('telescope').setup{ defaults = { preview = { enable = true, treesitter = false, }, vimgrep_arguments = { "ag", "--nocolor", "--noheading", "--numbers", "--column", "--smart-case", "--silent", "--follow", "--vimgrep", }, file_ignore_patterns = { "%.svg", "%.spl", "%.sug", "%.bmp", "%.gpg", "%.pub", "%.kbx", "%.db", "%.jpg", "%.jpeg", "%.gif", "%.png", "%.org_archive", "%.flf", ".cache", ".git/", ".thunderbird", ".nas", }, mappings = { i = { -- Required so that folding works when opening a file in telescope -- https://github.com/nvim-telescope/telescope.nvim/issues/559 ["<CR>"] = function() vim.cmd [[:stopinsert]] vim.cmd [[call feedkeys("\<CR>")]] end, ['<C-j>'] = 'move_selection_next', ['<C-k>'] = 'move_selection_previous', } } }, pickers = { find_files = { find_command = { "rg", "--files", "--hidden", "--glob", "!**/.git/*" }, hidden = true, follow = true, } }, extensions = { fzf = { fuzzy = true, -- false will only do exact matching override_generic_sorter = true, -- override the generic sorter override_file_sorter = true, -- override the file sorter case_mode = "smart_case", -- or "ignore_case" or "respect_case" -- the default case_mode is "smart_case" }, heading = { treesitter = true, }, } } require('telescope').load_extension('orgmode') local key = vim.keymap vim.g.mapleader = ' ' local builtin = require('telescope.builtin') key.set('n', '<leader>f', builtin.find_files, {}) key.set('n', '<leader>F', ':Telescope file_browser<cr>') vim.api.nvim_create_autocmd('FileType', { pattern = 'org', group = vim.api.nvim_create_augroup('orgmode_telescope_nvim', { clear = true }), callback = function() vim.keymap.set('n', '<leader>r', require('telescope').extensions.orgmode.refile_heading) vim.keymap.set('n', '<leader>g', require('telescope').extensions.orgmode.search_headings) end, }) require('orgmode').setup_ts_grammar() local org = require('orgmode').setup({ org_agenda_files = { "./*" }, org_todo_keywords = { 'TODO(t)', 'CHECK(c)', 'DOING(d)', 'RDEACTIVATED(r)', 'WAITING(w)', '|','DONE(e)', 'REJECTED(j)', 'DUPLICATE(u)' }, org_hide_leading_stars = true, org_deadline_warning_days = 0, win_split_mode = "horizontal", org_priority_highest = 'A', org_priority_default = 'C', org_priority_lowest = 'D', mappings = { global = { org_agenda = 'ga', org_capture = ';c', }, org = { -- Enter new items org_meta_return = '<c-cr>', org_insert_heading_respect_content = ';<cr>', org_insert_todo_heading = "<c-t>", org_insert_todo_heading_respect_content = ";t", -- Heading promoting and demoting org_toggle_heading = '<leader>h', org_do_promote = '<h', org_do_demote = '>h', org_promote_subtree = '<H', org_demote_subtree = '>H', -- Heading moving org_move_subtree_up = "<leader>k", org_move_subtree_down = "<leader>j", -- Heading navigation org_next_visible_heading = '<c-j>', org_previous_visible_heading = '<c-k>', org_forward_heading_same_level = '<c-n>', org_backward_heading_same_level = '<c-p>', outline_up_heading = 'gp', org_open_at_point = 'gx', -- State transition org_todo = 't', -- Priority change org_priority_up = '-', org_priority_down = '=', -- Date management org_deadline = '<leader>d', org_schedule = '<leader>s', org_time_stamp = ';d', org_change_date = '<c-e>', -- Tag management org_set_tags_command = 'T', -- Archive management and refiling org_archive_subtree = ';a', org_toggle_archive_tag = ';A', -- org_refile = '<leader>r', The refile is being handled below }, agenda = { org_agenda_later = '<c-n>', org_agenda_earlier = '<c-p>', org_agenda_switch_to = '<tab>', org_agenda_goto = '<cr>', org_agenda_priority_up = '=', org_agenda_set_tags = 'T', org_agenda_deadline = '<leader>d', org_agenda_schedule = '<leader>s', org_agenda_archive = 'a', }, capture = { org_capture_finalize = ';w', org_capture_refile = ';r', org_capture_kill = 'qqq', }, } }) local dap = require"dap" dap.configurations.lua = { { type = 'nlua', request = 'attach', name = "Attach to running Neovim instance", } } dap.adapters.nlua = function(callback, config) callback({ type = 'server', host = config.host or "127.0.0.1", port = config.port or 8086 }) end vim.api.nvim_set_keymap('n', '<leader>b', [[:lua require"dap".toggle_breakpoint()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>c', [[:lua require"dap".continue()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>n', [[:lua require"dap".step_over()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>m', [[:lua require"dap".repl.open()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>N', [[:lua require"dap".step_into()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<F12>', [[:lua require"dap.ui.widgets".hover()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<F5>', [[:lua require"osv".launch({port = 8086})<CR>]], { noremap = true })
-
New: Hide the state changes in the folds.
The folding of the recurring tasks iterations is also kind of broken. For the next example
** TODO Recurring task DEADLINE: <2024-02-08 Thu .+14d -0d> :PROPERTIES: :LAST_REPEAT: [2024-01-25 Thu 11:53] :END: - State "DONE" from "TODO" [2024-01-25 Thu 11:53] - State "DONE" from "TODO" [2024-01-10 Wed 23:24] - State "DONE" from "TODO" [2024-01-03 Wed 19:39] - State "DONE" from "TODO" [2023-12-11 Mon 21:30] - State "DONE" from "TODO" [2023-11-24 Fri 13:10] - [ ] Do X
When folded the State changes is not added to the Properties fold. It's shown something like:
** TODO Recurring task DEADLINE: <2024-02-08 Thu .+14d -0d> :PROPERTIES:... - State "DONE" from "TODO" [2024-01-25 Thu 11:53] - State "DONE" from "TODO" [2024-01-10 Wed 23:24] - State "DONE" from "TODO" [2024-01-03 Wed 19:39] - State "DONE" from "TODO" [2023-12-11 Mon 21:30] - State "DONE" from "TODO" [2023-11-24 Fri 13:10] - [ ] Do X
I don't know if this is a bug or a feature, but when you have many iterations it's difficult to see the task description. So it would be awesome if they could be included into the properties fold or have their own fold.
I've found though that if you set the
org_log_into_drawer = "LOGBOOK"
in the config this is fixed. -
Correction: Rename Task to Action.
To remove the productivity capitalist load from the concept
-
New: How to deal with recurring tasks that are not yet ready to be acted upon.
By default when you mark a recurrent task as
DONE
it will transition the date (either appointment,SCHEDULED
orDEADLINE
) to the next date and change the state toTODO
. I found it confusing because for meTODO
actions are the ones that can be acted upon right now. That's why I'm using the next states instead:INACTIVE
: Recurrent task which date is not yet close so you should not take care of it.READY
: Recurrent task which date is overdue, we acknowledge the fact and mark the date as inactive (so that it doesn't clobber the agenda).
The idea is that once an INACTIVE task reaches your agenda, either because the warning days of the
DEADLINE
make it show up, or because it's theSCHEDULED
date you need to decide whether to change it toTODO
if it's to be acted upon immediately or toREADY
and deactivate the date.INACTIVE
then should be the default state transition for the recurring tasks once you mark it asDONE
. To do this, set in your config:org_todo_repeat_to_state = "INACTIVE",
If a project gathers a list of recurrent subprojects or subactions it can have the next states:
READY
: If there is at least one subelement in stateREADY
and the rest areINACTIVE
TODO
: If there is at least one subelement in stateTODO
and the rest may haveREADY
orINACTIVE
INACTIVE
: The project is not planned to be acted upon soon.WAITING
: The project is planned to be acted upon but all its subelements are inINACTIVE
state.
-
New: Debug
doesn't go up in the jump list. It's because
is a synonym of , andorg_cycle
is mapped by default asIf you're used to use zc
then you can disable theorg_cycle
by setting the mappingorg_cycle = "<nop>"
. -
New: Python libraries.
org-rw
is a library designed to handle Org-mode files, offering the ability to modify data and save it back to the disk.- Pros:
- Allows modification of data and saving it back to the disk
-
Includes tests to ensure functionality
-
Cons:
- Documentation is lacking, making it harder to understand and use
- The code structure is complex and difficult to read
- Uses
unittest
instead ofpytest
, which some developers may prefer - Tests are not easy to read
- Last commit was made five months ago, indicating potential inactivity
- Not very popular, with only one contributor, three stars, and no forks
orgparse
is a more popular library for parsing Org-mode files, with better community support and more contributors. However, it has significant limitations in terms of editing and saving changes.- Pros:
- More popular with 13 contributors, 43 forks, and 366 stars
- Includes tests to ensure functionality
-
Provides some documentation, available here
-
Cons:
- Documentation is not very comprehensive
- Cannot write back to Org-mode files, limiting its usefulness for editing content
- The author suggests using inorganic to convert Org-mode entities to text, with examples available in doctests and the orger library.
inorganic
is not popular, with one contributor, four forks, 24 stars, and no updates in five years- The library is only 200 lines of code
- The
ast
is geared towards single-pass document reading. While it is possible to modify the document object tree, writing back changes is more complicated and not a common use case for the author.
Tree-sitter is a powerful parser generator tool and incremental parsing library. It can build a concrete syntax tree for a source file and efficiently update the syntax tree as the source file is edited.
- Pros:
- General enough to parse any programming language
- Fast enough to parse on every keystroke in a text editor
- Robust enough to provide useful results even in the presence of syntax errors
- Dependency-free, with a runtime library written in pure C
- Supports multiple languages through community-maintained parsers
- Used by Neovim, indicating its reliability and effectiveness
- Provides good documentation, available here
-
Python library, py-tree-sitter, simplifies the installation process
-
Cons:
- Requires installation of Tree-sitter and the Org-mode language parser separately
- The Python library does not handle the Org-mode language parser directly
To get a better grasp of Tree-sitter you can check their talks:
lazyblorg orgparser.py
is another tool for working with Org-mode files. However, I didn't look at it. -
Correction: Tweak area concept.
Model a group of projects that follow the same interest, roles or accountabilities. These are not things to finish but rather to use as criteria for analyzing, defining a specific aspect of your life and to prioritize its projects to reach a higher outcome. We'll use areas to maintain balance and sustainability on our responsibilities as we operate in the world. Areas' titles don't contain verbs as they don't model actions. An example of areas can be health, travels or economy.
To filter the projects by area I set an area tag that propagates downstream. To find the area documents easily I add a section in the
index.org
of the documentation repository. For example: -
New: Change the default org-todo-keywords.
orig = '''* NEW_TODO_STATE First entry * NEW_DONE_STATE Second entry''' doc = loads(orig, environment={ 'org-todo-keywords': "NEW_TODO_STATE | NEW_DONE_STATE" })
Orgzly⚑
- New: Migrate from Orgzly to Orgzly Revived.
Gancio⚑
-
Correction: Change the concept of
Task
forAction
.To remove the capitalist productive mindset from the concept
-
Correction: Action cleaning.
Marking steps as done make help you get an idea of the evolution of the action. It can also be useful if you want to do some kind of reporting. On the other hand, having a long list of done steps (specially if you have many levels of step indentation may make the finding of the next actionable step difficult. It's a good idea then to often clean up all done items.
- For non recurring actions use the
LOGBOOK
to move the done steps. for example:** DOING Do X :LOGBOOK: - [x] Done step 1 - [-] Doing step 2 - [x] Done substep 1 :END: - [-] Doing step 2 - [ ] substep 2
This way the
LOGBOOK
will be automatically folded so you won't see the progress but it's at hand in case you need it.- For recurring actions:
- Mark the steps as done
- Archive the todo element.
- Undo the archive.
- Clean up the done items.
This way you have a snapshot of the state of the action in your archive.
- For non recurring actions use the
-
New: Project cleaning.
Similar to action cleaning we want to keep the state clean. If there are not that many actions under the project we can leave the done elements as
DONE
, once they start to get clobbered up we can create aClosed
section.For recurring projects:
- Mark the actions as done
- Archive the project element.
- Undo the archive.
- Clean up the done items.
-
New: Trimester review.
The objectives of the trimester review are:
- Identify the areas to focus on for the trimester
- Identify the tactics you want to use on those areas.
- Review the previous trimester tactics
The objectives are not:
- To review what you've done or why you didn't get there.
When to do the trimester reviews
As with personal integrity review, it's interesting to do analysis at representative moments. It gives it an emotional weight. You can for example use the solstices or my personal version of the solstices:
- Spring analysis (1st of March): For me the spring is the real start of the year, it's when life explodes after the stillness of the winter. The sun starts to set later enough so that you have light in the afternoons, the climate gets warmer thus inviting you to be more outside, the nature is blooming new leaves and flowers. It is then a moment to build new projects and set the current year on track.
- Summer analysis (1st of June): I hate heat, so summer is a moment of retreat. Everyone temporarily stop their lives, we go on holidays and all social projects slow their pace. Even the news have even less interesting things to report. It's so hot outside that some of us seek the cold refuge of home or remote holiday places. Days are long and people love to hang out till late, so usually you wake up later, thus having less time to actually do stuff. Even in the moments when you are alone the heat drains your energy to be productive. It is then a moment to relax and gather forces for the next trimester. It's also perfect to develop easy and chill personal projects that have been forgotten in a drawer. Lower your expectations and just flow with what your body asks you.
- Autumn analysis (1st of September): September it's another key moment for many people. We have it hardcoded in our life since we were children as it was the start of school. People feel energized after the summer holidays and are eager to get back to their lives and stopped projects. You're already 6 months into the year, so it's a good moment to review your year plan and decide how you want to invest your energy reserves.
- Winter analysis (1st of December): December is the cue that the year is coming to an end. The days grow shorter and colder, they basically invite you to enjoy a cup of tea under a blanket. It is then a good time to get into your cave and do an introspection analysis on the whole year and prepare the ground for the coming year. Some of the goals of this season are:
- Think everything you need to guarantee a good, solid and powerful spring start.
- Do the year review to adjust your principles.
The year is then divided in two sets of an expansion trimester and a retreat one. We can use this information to adjust our life plan accordingly. In the expansion trimester we could invest more energies in the planning, and in the retreat ones we can do more throughout reviews.
Listen to your desires
The trimester review requires an analysis that doesn't fill in a day session. It requires slow thinking over some time. So I'm creating a task 10 days before the actual review to start thinking about the next trimester. Whether it's ideas, plans, desires, objectives, values, or principles.
Is useful for that document to be available wherever you go, so that in any spare time you can pop it up and continue with the train of thought.
Doing the reflection without seeing your life path prevents you from being tainted by it, thus representing the real you of right now.
On the day to actually do the review, follow the steps of the Month review prepare adjusting them to the trimester case.
Answer some meaningful guided questions
To be done, until then you can read chapters 13, 14 and the epilogue of the book Four thousand weeks by Oliver Burkman.
Refactor your gathered thoughts
If you've followed the prepare steps, you've already been making up your mind on what do you want the next trimester to look like. Now it's the time to refine those thoughts.
In your roadmap document add a new section for the incoming trimester similar to:
Go one by one (don't peek!) of your gathered items and translate them in the next sections:* Roadmap ** 2024 *** Summer 2024 **** Essential intent **** Trimester analysis **** Trimester objectives ***** TODO Objective 1 ****** TODO SubObjective 1
Trimester analysis
: A text with as many paragraphs as you need to order your thoughtsTrimester objectives
: These can be concrete emotional projects you want to carry through.Essential intent
: This is the main headline of your trimester, probably you won't be able to define it until the last parts of the review process. It should be concrete and emotional too, it's going to be the idea that gives you strength on your weak moments and your guide to decide which projects to do and which not to.
Don't be too concerned on the format of the content of the objectives, this is the first draft, and we'll refine it through the planning.
-
New: Wordpress plugin.
This plugin allows you to embed a list of events or a single event from your Gancio website using a shortcode. It also allows you to connects a Gancio instance to a your wordpress website to automatically push events published on WordPress: for this to work an event manager plugin is required, Event Organiser and The Events Calendar are supported. Adding another plugin it’s an easy task and you have a guide available in the repo that shows you how to do it.
The source code of the plugin is in the wp-plugin directory of the official repo
Habit management⚑
-
New: Introduce habit management.
A habit is a routine of behavior that is repeated regularly and tends to occur subconsciously.
A 2002 daily experience study found that approximately 43% of daily behaviors are performed out of habit. New behaviours can become automatic through the process of habit formation. Old habits are hard to break and new habits are hard to form because the behavioural patterns that humans repeat become imprinted in neural pathways, but it is possible to form new habits through repetition.
When behaviors are repeated in a consistent context, there is an incremental increase in the link between the context and the action. This increases the automaticity of the behavior in that context. Features of an automatic behavior are all or some of: efficiency, lack of awareness, unintentionality, and uncontrollability.
Mastering habit formation can be a powerful tool to change yourself. Usually with small changes you get massive outcomes in the long run. The downside is that it's not for the impatient people as it often appears to make no difference until you cross a critical threshold that unlocks a new level of performance.
-
New: Why are habits interesting.
Whenever you face a problem repeatedly, your brain begins to automate the process of solving it. Habits are a series of automatic resolutions that solve the problems and stresses you face regularly.
As habits are created, the level of activity in the brain decreases. You learn to lock in on the cues that predict success and tune out everything else. When a similar situation arises in the future, you know exactly what you look for. There is no longer a need to analyze every angle of a situation. Your brain skips the process of trial and error and creates a mental rule: if this, then that.
Habit formation is incredibly useful because the conscious mind is the bottleneck of the brain. It can only pay attention to one problem at a time. Habits reduce the cognitive load and free up mental capacity, so they can be carried on with your nonconscious mind and you can allocate your attention to other tasks.
-
New: Identity focused changes.
Changing our habits is challenging because we try to change the wrong thing in the wrong way.
There are three levels at which change can occur:
- Outcomes: Changing your results. Goals fall under this category: publishing a book, run daily
- Process: Changing your habits and systems: decluttering your desk for a better workflow, developing a meditation practice.
- Identity: Changing your beliefs, assumptions and biases: your world view, your self-image, your judgments.
Many people begin the process of changing their habits by focusing on what they want to achieve. This leads to outcome-based habits. The alternative is to build identity-based habits. With this approach, we start by focusing on who we wish to become.
The first path of change is doomed because maintaining behaviours that are incongruent with the self is expensive and will not last. Even if they make rational sense. Thus it's hard to change your habits if you never change the underlying beliefs that led to your past behaviour. On the other hand it's easy to find motivation once a habit has changed your identity as you may be proud of it and will be willing to maintain all the habits and systems associated with it. For example: The goal is not to read a book, but to become a reader.
Focusing on outcomes may also bring the next problems:
- Focusing on the results may lead you to temporal solutions. If you focus on the source of the issue at hand you may solve it with less effort and get you to a more stable one.
- Goals create an "either-or" conflict: either you achieve your goal and are successful or you fail and you are disappointed. Thus you only get a positive reward if you fulfill a goal. If you instead focus on the process rather than the result, you will be satisfied anytime your system is running.
- When your hard work is focused on a goal you may feel depleted once you meet it and that could make you loose the condition that made you meet the goal in the first place.
Research has shown that once a person believes in a particular aspect of their identity, they are more likely to act in alignment with that belief. This of course is a double-edged sword. Identity change can be a powerful force for self-improvement. When working against you, identity change can be a curse.
-
Whatever your identity is right now, you only believe it because you have proof of it. The more evidence you have for a belief, the more strongly you will believe it.
Your habits and systems are how you embody your identity. When you make your bed each day, you embody the identity of an organized person. The more you repeat a behaviour, the more you reinforce the identity associated with that behaviour. To the point that your self-image begins to change. The effect of one-off experiences tends to fade away while the effect of habits gets reinforced with time, which means your habits contribute most of the evidence that shapes your identity.
Every action you take is a vote for the type of person you wish to become. This is one reason why meaningful change does not require radical change. Small habits can make a meaningful difference by providing evidence of a new identity.
Once you start the ball rolling things become easier as building habits is a feedback loop. Your habits shape your identity, and your identity shapes your habits.
The most practical way to change the identity is to:
- Decide the type of person you want to be
- Prove it to yourself with small wins
Another advantage of focusing in what type of person you want to be is that maybe the outcome you wanted to focus on is not the wisest smallest step to achieve your identity change. Thinking on the identity you want to embrace can make you think outside the box.
-
New: Decide the type of person you want to be.
One way to decide the person you want to be is to answer big questions like: what do you want to stand for? What are your principles and values? Who do you wish to become?
As we're more result oriented, another way is to work backwards from them to the person you want to be. Ask yourself: Who is the type of person that could get the outcome I want?
-
The process of building a habit from a behaviour can be divided into four stages:
- Reward is the end goal.
- Cue is the trigger in your brain that initiate a behaviour. It's contains the information that predicts a reward.
- Cravings are the motivational force fueled by the desire of the reward. Without motivation we have no reason to act.
- Response is the thought or action you perform to obtain the reward. The response depends on the amount of motivation you have, how much friction is associated with the behaviour and your ability to actually do it.
If a behaviour is insufficient in any of the four stages, it will not become a habit. Eliminate the cue and your habit will never start. Reduce the craving and you won't have enough motivation to act. Make the behaviour difficult and you won't be able to do it. And if the reward fails to satisfy your desire, then you'll have no reason to do it again in the future.
We chase rewards because they:
- Deliver contentment.
- Satisfy your craving.
- Teach us which actions are worth remembering in the future.
If a reward is met then it becomes associated with the cue, thus closing the habit feedback loop.
If we keep these stages in mind then:
-
To build good habits we need to:
- Cue: Make it obvious
- Craving: Make it attractive
- Response: Make it easy
- Reward: Make it satisfying
-
To break bad habits we need to:
- Cue: Make it invisible
- Craving: Make it unattractive
- Response: Make it difficult
- Reward: Make it unsatisfying
-
New: Select which habits you want to work with.
Our responses to the cues are so deeply encoded that it may feel like the urge to act comes from nowhere. For this reason, we must begin the process of behavior change with awareness. Before we can effectively build new habits, we need to get a handle on our current ones. The author suggests to do a list of your daily habits and rate them positively, negatively or neutral under the judgement of whether it brings you closer to the desired person you want to be.
I find this approach expensive time-wise if you already have a huge list of habits to work with. As it's my case I'll skip this part. You can read it in more detail in the chapter "4: The Man Who Didn't Look Right".
-
New: Working with the habit cues.
The first place to start the habit design is to understand and tweak the triggers that produce them. We'll do it by:
-
New: Clearly formulate the habit you want to change.
The cues that can trigger an habit can come in a wide range of forms but the two most common are time and location. Being specific about what you want and how you will achieve it helps you say no to things that derail progress, distract your attention and pull you off course. And with enough repetition, you will get the urge to do the right thing at the right time, even if you can't say why. That's why it's interesting to formulate your habits as "I will [behaviour] at [time] in [location]".
You want the cue to be highly specific and immediately actionable. If there is room for doubt the implementation will suffer. Continuously refine the habit definitions as you catch the exceptions that drift you off.
If you aren't sure of when to start your habit, try the first day of the week, month or year. People are more likely to take action at those times because hope is usually higher as you get the feeling of a fresh start.
-
New: Habit stacking.
Many behaviours are linked together where the action of the first is the cue that triggers the next one. You can use this connection to build new habits based on your established ones. This may be called habit stacking. The formulation in this case is "After [current habit], I will [new habit]".
The key is to tie your desired behaviour into something you already do each day. Once you have mastered this basic structure, you can begin to create larger stacks by chaining small habits together. The catch is that the new habit should have the same frequency as the established one.
One way to find the right trigger for your habit stack is by brainstorming over:
- The list of your current habits.
- A new list of things that always happen to you with that frequency.
With these two lists, you can begin searching for the best triggers for the stack.
-
New: Use the environment to tweak your cues.
The cues that trigger a habit can start out very specific, but over time your habits become associated not with a single trigger but with the entire context surrounding the behaviour. This stacks over itself and your habits change depending on the room you are in and the cues in front of you. The context or the environment is then the invisible hand that shapes behaviours. They are not defined by the objects in the environment but by our relationship to them.
A new environment is a good foundation to make new habits, as you are free from the subtle triggers that nudge you toward your current habits. When you can't manage to get an entirely new environment, you can redefine or rearrange your current one.
When building good habits you can rearrange the environment to create obvious visual cues that draw your attention towards the desired habit. By sprinkling triggers throughout your surroundings, you increase the odds that you'll think about your habit throughout the day.
Once a habit has been encoded, the urge to act follows whenever the environmental cues reappear. This is why bad habits reinforce themselves. As you carry through the behaviour you spiral into a situation where the craving keeps growing and points you to keep on going with the same response. For example watching TV makes you feel sluggish, so you watch more television because you don't have the energy to do anything else.
Even if you manage to break a habit, you are unlikely to forget it's cues even if you don't do it for a while. That means that simply resisting temptation is an ineffective strategy. In the short run it may work. In the long run, as self-control is an exhausting task that consumes willpower, we become a product of the environment we live in. Trying to change a habit with self-control is doomed to fail as you may be able to resist temptation once or twice, but it's unlikely you can muster the willpower to override your desires every time. It's also very hard and frustrating to try to achieve change when you're under the mood influences of a bad habit.
A more reliable approach is to cut bad habits off at the source. Tweak the environment to make the cue virtually impossible to happen. That way you won't even have the chance to fall for the craving.
-
New: Temptation bundling.
Dopamine is a neurotransmitter that can be used as the scientific measurement of craving. For years we assumed that it was all about pleasure, but now we know it plays a central role in many neurological processes, including motivation, learning and memory, punishment and aversion and voluntary movement.
Habits are a dopamine-driven feed back loop. It is released not only when you receive a reward but also when you anticipate it. This anticipation, and not the fulfillment of it, is what gets us to take action.
If we make a habit more attractive it will release more dopamine which will gives us more motivation to carry it through.
Temptation bundling works by pairing an action you want to do with an action you need to do. You're more likely to find a behaviour attractive if you get to do one of your favourite things at the same time. In the end you may even look forward to do the habit you need as it's related to the habit you want.
-
New: Align your personal identity change with an existent shared identity.
We pick up habits from the people around us. As a general rule, the closer we are to someone, the more likely we are to imitate some of their habits. One of the most effective things you can do to build better habits is to join a culture where your desired behaviour is the normal one. This transforms your personal identity transformation into the building of a shared one. Shared identities have great benefits over single ones:
- They foster belonging. A powerful feeling that creates motivation.
- They are more resilient: When one falters others will take their place so all together you'll guarantee the maintenance of the identity.
- They create friendship and community
- They expose you to an environment where more habits tied to that identity thrive.
Likewise, if you're trying to run from a bad habit cut your ties to communities that embrace that habit.
-
New: Track your habit management.
You can have a
habits.org
file where you prioritize, analyze, track them.I'm using the next headings:
- Habits being implemented: It's subdivided in two:
- Habits that need attention
- Habits that don't need attention
- Unclassified habits: Useful when refiling habits from your inbox. This list will be analyzedwhen you do habit analysis.
- Backlog of habits: Unrefined and unordered list of habits
- Implemented habits:
- Rejected habits:
Each habit is a
TODO
item with the usual states:TODO
,DOING
,DONE
,REJECTED
. In it's body I keep a log of the evolution and the analysis of the habit. -
New: Habit management workflow.
Each month I'm trying to go through the list of habits to:
- Update the state of the habits: Some will be done, rejected or to register ideas about them.
- Decide which ones need attention.
- Do habit analysis on the ones that need attention.
For each of the habits that need analysis, apply the learnings of the next sections:
Calendar management⚑
-
New: Add calendar event notification system tool.
Set up a system that notifies you when the next calendar event is about to start to avoid spending mental load on it and to reduce the possibilities of missing the event.
I've created a small tool that:
- Tells me the number of pomodoros that I have until the next event.
- Once a pomodoro finishes it makes me focus on the amount left so that I can prepare for the event
- Catches my attention when the event is starting.
Life chores management⚑
himalaya⚑
-
New: Doing the inventory review.
I haven't found a way to make the grocy inventory match the reality because for me it's hard to register when I consume a product. Even more if other people also use them. Therefore I use grocy only to know what to buy without thinking about it. For that use case the inventory needs to meet reality only before doing the groceries. I usually do a big shopping of non-perishable goods at the supermarket once each two or three months, and a weekly shopping of the rest.
Tracking the goods that are bought each week makes no sense as those are things that are clearly seen and are very variable depending on the season. Once I've automated the ingestion and consumption of products it will, but so far it would mean investing more time than the benefit it gives.
This doesn't apply to the big shopping, as this one is done infrequently, so we need a better planning.
To do the inventory review I use a tablet and the android app.
- Open the stock overview and iterate through the locations to:
- Make sure that the number of products match the reality
- Iterate over the list of products checking the quantity
- Look at the location to see if there are missing products in the inventory
- Adjust the product properties (default location, minimum amount)
- Check the resulting shopping list and adjust the minimum values.
- Check the list of missing products to adjust the minimum values. I have a notepad in the fridge where I write the things I miss.
-
New: Introduce route management.
To analyze which hiking routes are available in a zone I'm following the next process
- Check in my
trips
orgmode directory to see if the zone has already been indexed. - Do a first search of routes
- Identify which books or magazines describe the zone
- For each of the described routes in each of these books:
- Create the
Routes
section with tag:route:
if it doesn't exist - Fill up the route form in a
TODO
heading. Something similar to:Reference: Book Page Source: Where does it start Distance: X km Slope: X m Type: [Lineal/Circular/Semi-lineal] Difficulty: Track: URL (only if you don't have to search for it)
- Add tags of the people I'd like to do it with
- Put a postit on the book/magazine if it's likely I'm going to do it
- Open a web maps tab with the source of the route to calculate the time from the different lodgins
- Create the
-
If there are not enough, repeat the process above for each of your online route reference blogs
-
Choose the routes to do
- Show the gathered routes to the people you want to go with
-
Select which ones you'll be more likely to do
-
For each of the chosen routes
- Search the track in wikiloc if it's missing
- Import the track in OsmAnd+
- Check in my
-
New: Add API and python library docs.
There is no active python library, although it existed pygrocy
-
New: Introduce himalaya.
himalaya is a Rust CLI to manage emails.
Features:
- Multi-accounting
- Interactive configuration via wizard (requires
wizard
feature) - Mailbox, envelope, message and flag management
- Message composition based on
$EDITOR
- IMAP backend (requires
imap
feature) - Maildir backend (requires
maildir
feature) - Notmuch backend (requires
notmuch
feature) - SMTP backend (requires
smtp
feature) - Sendmail backend (requires
sendmail
feature) - Global system keyring for managing secrets (requires
keyring
feature) - OAuth 2.0 authorization (requires
oauth2
feature) - JSON output via
--output json
- PGP encryption:
- via shell commands (requires
pgp-commands
feature) - via GPG bindings (requires
pgp-gpg
feature) - via native implementation (requires
pgp-native
feature)
Cons:
- Documentation is inexistent, you have to dive into the
--help
to understand stuff.
The
v1.0.0
is currently being tested on themaster
branch, and is the prefered version to use. Previous versions (including GitHub beta releases and repositories published versions) are not recommended.Himalaya CLI
v1.0.0
can be installed with a pre-built binary. Find the latestpre-release
GitHub workflow and look for the Artifacts section. You should find a pre-built binary matching your OS.Himalaya CLI
v1.0.0
can also be installed with cargo:Configuration$ cargo install --git https://github.com/pimalaya/himalaya.git --force himalaya
Just run
himalaya
, the wizard will help you to configure your default account.You can also manually edit your own configuration, from scratch:
- Copy the content of the documented
./config.sample.toml
- Paste it in a new file
~/.config/himalaya/config.toml
- Edit, then comment or uncomment the options you want
If using mbrsync
My generic configuration for an mbrsync account is:
[accounts.account_name] email = "lyz@example.org" display-name = "lyz" envelope.list.table.unseen-char = "u" envelope.list.table.replied-char = "r" backend.type = "maildir" backend.root-dir = "/home/lyz/.local/share/mail/lyz-example" backend.maildirpp = false message.send.backend.type = "smtp" message.send.backend.host = "example.org" message.send.backend.port = 587 message.send.backend.encryption = "start-tls" message.send.backend.login = "lyz" message.send.backend.auth.type = "password" message.send.backend.auth.command = "pass show mail/lyz.example"
Once you've set it then you need to fix the INBOX directory.
Then you can check if it works by running
himalaya envelopes list -a lyz-example
Vim plugin installation
Using lazy:
return { { "pimalaya/himalaya-vim", }, }
You can then run
:Himalaya account_name
and it will open himalaya in your editor.Configure the account bindings
To avoid typing
:Himalaya account_name
each time you want to check the email you can set some bindings:return { { "pimalaya/himalaya-vim", keys = { { "<leader>ma", "<cmd>Himalaya account_name<cr>", desc = "Open account_name@example.org" }, { "<leader>ml", "<cmd>Himalaya lyz<cr>", desc = "Open lyz@example.org" }, }, }, }
Setting the description is useful to see the configured accounts with which-key by typing
<leader>m
and waiting.Configure extra bindings
The default plugin doesn't yet have all the bindings I'd like so I've added the next ones:
- In the list of emails view:
dd
in normal mode ord
in visual: Delete emails-
q
: exit the program -
In the email view:
d
: Delete emailq
: Return to the list of emails view
If you want them too set the next config:
return { { "pimalaya/himalaya-vim", config = function() vim.api.nvim_create_augroup("HimalayaCustomBindings", { clear = true }) vim.api.nvim_create_autocmd("FileType", { group = "HimalayaCustomBindings", pattern = "himalaya-email-listing", callback = function() -- Bindings to delete emails vim.api.nvim_buf_set_keymap(0, "n", "dd", "<plug>(himalaya-email-delete)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "x", "d", "<plug>(himalaya-email-delete)", { noremap = true, silent = true }) -- Bind `q` to close the window vim.api.nvim_buf_set_keymap(0, "n", "q", ":bd<CR>", { noremap = true, silent = true }) end, }) vim.api.nvim_create_augroup("HimalayaEmailCustomBindings", { clear = true }) vim.api.nvim_create_autocmd("FileType", { group = "HimalayaEmailCustomBindings", pattern = "mail", callback = function() -- Bind `q` to close the window vim.api.nvim_buf_set_keymap(0, "n", "q", ":q<CR>", { noremap = true, silent = true }) -- Bind `d` to delete the email and close the window vim.api.nvim_buf_set_keymap( 0, "n", "d", "<plug>(himalaya-email-delete):q<CR>", { noremap = true, silent = true } ) end, }) end, }, }
Configure email fetching from within vim
Fetching emails from within vim is not yet supported, so I'm manually refreshing by account:
return { { "pimalaya/himalaya-vim", keys = { -- Email refreshing bindings { "<leader>rj", ':lua FetchEmails("lyz")<CR>', desc = "Fetch lyz@example.org" }, }, config = function() function FetchEmails(account) vim.notify("Fetching emails for " .. account .. ", please wait...", vim.log.levels.INFO) vim.cmd("redraw") vim.fn.jobstart("mbsync " .. account, { on_exit = function(_, exit_code, _) if exit_code == 0 then vim.notify("Emails for " .. account .. " fetched successfully!", vim.log.levels.INFO) else vim.notify("Failed to fetch emails for " .. account .. ". Check the logs.", vim.log.levels.ERROR) end end, }) end end, }, }
You still need to open again
:Himalaya account_name
as the plugin does not reload if there are new emails.Show notifications when emails arrive
You can set up mirador to get those notifications.
Not there yet
- With the vim plugin you can't switch accounts
- Let the user delete emails without confirmation
- Fetching emails from within vim
Troubleshooting
Cannot find maildir matching name INBOX
mbrsync
usesInbox
instead of the defaultINBOX
so it doesn't find it. In theory you can usefolder.alias.inbox = "Inbox"
but it didn't work with me, so I finally ended up doing a symbolic link fromINBOX
toInbox
.Cannot find maildir matching name Trash
That's because the
Trash
directory does not follow the Maildir structure. I had to create thecur
tmp
andnew
directories.References - Source - Vim plugin source
-
New: Introduce mailbox.
mailbox
is a python library to work with MailDir and mbox local mailboxes.It's part of the core python libraries, so you don't need to install anything.
Usage
The docs are not very pleasant to read, so I got most of the usage knowledge from these sources:
One thing to keep in mind is that an account can have many mailboxes (INBOX, Sent, ...), there is no "root mailbox" that contains all of the other
initialise a mailbox
mbox = mailbox.Maildir('path/to/your/mailbox')
Where the
path/to/your/mailbox
is the directory that contains thecur
,new
, andtmp
directories.Working with mailboxes
It's not very clear how to work with them, the Maildir mailbox contains the emails in iterators
[m for m in mbox]
, it acts kind of a dictionary, you can get the keys of the emails with[k for k in mbox.iterkeys]
, and then you canmbox[key]
to get an email, you cannot modify those emails (flags, subdir, ...) directly in thembox
object (for examplembox[key].set_flags('P')
doesn't work). You need tomail = mbox.pop(key)
, do the changes in themail
object and thenmbox.add(mail)
it again, with the downside that after you added it again, thekey
has changed! But it's the return value of theadd
method.If the program gets interrupted between the
pop
and theadd
then you'll loose the email. The best way to work with it would be then:mail = mbox.get(key)
the email- Do all the process you need to do with the email
mbox.pop(key)
andkey = mbox.add(mail)
In theory
mbox
has anupdate
method that does this, but I don't understand it and it doesn't work as expected :S.Moving emails around
You can't just move the files between directories like you'd do with python as each directory contains it's own identifiers.
Moving a message between the maildir directories
The
Message
has aset_subdir
Even though you can create folders with
mailbox
it creates them in a way that mbsync doesn't understand it. It's easier to manually create thecur
,tmp
, andnew
directories. I'm using the next function:if not (mailbox_dir / "cur").exists(): for dir in ["cur", "tmp", "new"]: (mailbox_dir / dir).mkdir(parents=True) log.info(f"Initialized mailbox: {mailbox}") else: log.debug(f"{mailbox} already exists")
References
-
New: Introduce maildir.
The Maildir e-mail format is a common way of storing email messages on a file system, rather than in a database. Each message is assigned a file with a unique name, and each mail folder is a file system directory containing these files.
A Maildir directory (often named Maildir) usually has three subdirectories named
tmp
,new
, andcur
.- The
tmp
subdirectory temporarily stores e-mail messages that are in the process of being delivered. This subdirectory may also store other kinds of temporary files. - The
new
subdirectory stores messages that have been delivered, but have not yet been seen by any mail application. - The
cur
subdirectory stores messages that have already been seen by mail applications.
References
- The
-
New: My emails are not being deleted on the source IMAP server.
That's the default behavior of
mbsync
, if you want it to actually delete the emails on the source you need to add:Under your channel (close toExpunge Both
Sync All
,Create Both
) -
New: Mbsync error: UID is beyond highest assigned UID.
If during the sync you receive the following errors:
mbsync error: UID is 3 beyond highest assigned UID 1
Go to the place where
mbsync
is storing the emails and find the file that is giving the error, you need to find the files that containU=3
, imagine that it's something like1568901502.26338_1.hostname,U=3:2,S
. You can strip off everything from the,U=
from that filename and resync and it should be fine, e.g.feat(mirador): introduce miradormv '1568901502.26338_1.hostname,U=3:2,S' '1568901502.26338_1.hostname'
DEPRECATED: as of 2024-11-15 the tool has many errors (1, 2), few stars (4) and few commits (8). use watchdog instead and build your own solution.
mirador is a CLI to watch mailbox changes made by the maintaner of himalaya.
Features:
- Watches and executes actions on mailbox changes
- Interactive configuration via wizard (requires
wizard
feature) - Supported events: on message added.
- Supported actions: send system notification, execute shell command.
- Supports IMAP mailboxes (requires
imap
feature) - Supports Maildir folders (requires
maildir
feature) - Supports global system keyring to manage secrets (requires
keyring
feature) - Supports OAuth 2.0 (requires
oauth2
feature)
Mirador CLI is written in Rust, and relies on cargo features to enable or disable functionalities. Default features can be found in the
features
section of theCargo.toml
.The
v1.0.0
is currently being tested on themaster
branch, and is the preferred version to use. Previous versions (including GitHub beta releases and repositories published versions) are not recommended.*Cargo (git)
Mirador CLI
v1.0.0
can also be installed with cargo:$ cargo install --frozen --force --git https://github.com/pimalaya/mirador.git
Pre-built binary
Mirador CLI
v1.0.0
can be installed with a pre-built binary. Find the latestpre-release
GitHub workflow and look for the Artifacts section. You should find a pre-built binary matching your OS.Configuration
Just run
mirador
, the wizard will help you to configure your default account.You can also manually edit your own configuration, from scratch:
- Copy the content of the documented
./config.sample.toml
- Paste it in a new file
~/.config/mirador/config.toml
-
Edit, then comment or uncomment the options you want
-
New: Configure navigation bindings.
The default bindings conflict with my git bindings, and to make them similar to orgmode agenda I'm changing the next and previous page:
return { { "pimalaya/himalaya-vim", keys = { { "b", "<plug>(himalaya-folder-select-previous-page)", desc = "Go to the previous email page" }, { "f", "<plug>(himalaya-folder-select-next-page)", desc = "Go to the next email page" }, }, }, }
-
Correction: Configure the account bindings.
-
Correction: Tweak the bindings.
Move forward and backwards in the history of emails:
Better bindings for the email list view:vim.api.nvim_create_autocmd("FileType", { group = "HimalayaCustomBindings", pattern = "himalaya-email-listing", callback = function() vim.api.nvim_buf_set_keymap(0, "n", "b", "<plug>(himalaya-folder-select-previous-page)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "n", "f", "<plug>(himalaya-folder-select-next-page)", { noremap = true, silent = true }) end, })
feat(himalaya#Searching emails): Searching emails-- Refresh emails vim.api.nvim_buf_set_keymap(0, "n", "r", ":lua FetchEmails()<CR>", { noremap = true, silent = true }) -- Email list view bindings vim.api.nvim_buf_set_keymap(0, "n", "b", "<plug>(himalaya-folder-select-previous-page)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "n", "f", "<plug>(himalaya-folder-select-next-page)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "n", "R", "<plug>(himalaya-email-reply-all)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "n", "F", "<plug>(himalaya-email-forward)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "n", "m", "<plug>(himalaya-folder-select)", { noremap = true, silent = true }) vim.api.nvim_buf_set_keymap(0, "n", "M", "<plug>(himalaya-email-move)", { noremap = true, silent = true })
You can use the
g/
binding from within nvim to search for emails. The query syntax supports filtering and sorting query.I've tried changing it to
/
without success :'(Filters
A filter query is composed of operators and conditions. There is 3 operators and 8 conditions:
not <condition>
: filter envelopes that do not match the condition<condition> and <condition>
: filter envelopes that match both conditions<condition> or <condition>
: filter envelopes that match one of the conditionsdate <yyyy-mm-dd>
: filter envelopes that match the given datebefore <yyyy-mm-dd>
: filter envelopes with date strictly before the given oneafter <yyyy-mm-dd>
: filter envelopes with date stricly after the given onefrom <pattern>
: filter envelopes with senders matching the given patternto <pattern>
: filter envelopes with recipients matching the given patternsubject <pattern>
: filter envelopes with subject matching the given patternbody <pattern>
: filter envelopes with text bodies matching the given patternflag <flag>
: filter envelopes matching the given flag
Sorting A sort query starts by "order by", and is composed of kinds and orders. There is 4 kinds and 2 orders:
date [order]
: sort envelopes by datefrom [order]
: sort envelopes by senderto [order]
: sort envelopes by recipientsubject [order]
: sort envelopes by subject<kind> asc
: sort envelopes by the given kind in ascending order<kind> desc
: sort envelopes by the given kind in descending order
Examples
subject foo and body bar
: filter envelopes containing "foo" in their subject and "bar" in their text bodiesorder by date desc subject
: sort envelopes by descending date (most recent first), then by ascending subjectsubject foo and body bar order by date desc subject
: combination of the 2 previous examples -
New: Troubleshoot cannot install the program.
Sometimes the installation steps fail as it's still not in stable. A workaround is to download the binary created by the pre-release CI. You can do it by:
- Click on the latest job
- Click on jobs
- Click on the job of your architecture
- Click on "Upload release"
- Search for "Artifact download URL" and download the file
- Unpack it and add it somewhere in your
$PATH
alot⚑
-
New: Comments.
Any text on a line after the character
;
is ignored, text like this:; I paid and left the taxi, forgot to take change, it was cold. 2015-01-01 * "Taxi home from concert in Brooklyn" Assets:Cash -20 USD ; inline comment Expenses:Taxi
-
New: Introduce matrix_highlight.
Matrix Highlight is a decentralized and federated way of annotating the web based on Matrix.
Think of it as an open source alternative to hypothesis.
It's similar to Populus but for the web.
I want to try it and investigate further specially if you can:
- Easily extract the annotations
- Activate it by default everywhere
-
New: Get the schema of a table.
\d+ table_name
-
New: Get the last row of a table.
SELECT * FROM Table ORDER BY ID DESC LIMIT 1
-
New: Introduce beanSQL.
-
New: Get the quarter of a date.
Use the
quarter(date)
selector in theSELECT
. For example:SELECT quarter(date) as quarter, sum(position) AS value WHERE account ~ '^Expenses:' OR account ~ '^Income:' GROUP BY quarter
It will return the quarter in the format
YYYY-QX
. -
New: Building your own dashboards.
I was wondering whether to create fava dashboards or to create them directly in grafana.
Pros of fava dashboards: - They are integrated in fava so it would be easy to browse other beancount data. Although this could be done as well in another window if I used grafana. - There is no need to create the beancount grafana data source logic. - It's already a working project, I would need just to tweak an existent example.
Cons: - I may need to learn echarts and write JavaScript to tweak some of the dashboards. - I wouldn't have all my dashboards in the same place. - It only solves part of the problem, I'd still need to write the bean-sql queries. But using beanql is probably the best way to extract data from beancount anyway. - It involves more magic than using grafana. - grafana dashboards are prettier. - I wouldn't use the grafana knowledge. - I'd learn a new tool only to use it here instead of taking the chance to improve my grafana skillset.
I'm going to try with fava dashboards and see how it goes
-
Correction: Deprecate in favour of himalaya.
DEPRECATED: Use himalaya instead.
-
New: Automatically sync emails.
I have many emails, and I want to fetch them with different frequencies, in the background and be notified if anything goes wrong.
For that purpose I've created a python script, a systemd service and some loki rules to monitor it.
Script to sync emails and calendars with different frequencies
The script iterates over the configured accounts in
accounts_config
and runsmbsync
for email accounts andvdirsyncer
for email accounts based on some cron expressions. It logs the output inlogfmt
format so that it's easily handled by lokiTo run it you'll first need to create a virtualenv, I use
mkvirtualenv account_syncer
which creates a virtualenv in~/.local/share/virtualenv/account_syncer
.Then install the dependencies:
pip install aiocron
Then place this script somewhere, for example (
~/.local/bin/account_syncer.py
)import asyncio import logging from datetime import datetime import asyncio.subprocess import aiocron accounts_config = { "emails": [ { "account_name": "lyz", "cron_expressions": ["*/15 9-23 * * *"], }, { "account_name": "work", "cron_expressions": ["*/60 8-17 * * 1-5"], # Monday-Friday }, { "account_name": "monitorization", "cron_expressions": ["*/5 * * * *"], }, ], "calendars": [ { "account_name": "lyz", "cron_expressions": ["*/15 9-23 * * *"], }, { "account_name": "work", "cron_expressions": ["*/60 8-17 * * 1-5"], # Monday-Friday }, ], } class LogfmtFormatter(logging.Formatter): """Custom formatter to output logs in logfmt style.""" def format(self, record: logging.LogRecord) -> str: log_message = ( f"level={record.levelname.lower()} " f"logger={record.name} " f'msg="{record.getMessage()}"' ) return log_message def setup_logging(logging_name: str) -> logging.Logger: """Configure logging to use logfmt format. Args: logging_name (str): The logger's name and identifier in the systemd journal. Returns: Logger: The configured logger. """ console_handler = logging.StreamHandler() logfmt_formatter = LogfmtFormatter() console_handler.setFormatter(logfmt_formatter) logger = logging.getLogger(logging_name) logger.setLevel(logging.INFO) logger.addHandler(console_handler) return logger log = setup_logging("account_syncer") async def run_mbsync(account_name: str) -> None: """Run mbsync command asynchronously for email accounts. Args: account_name (str): The name of the email account to sync. """ command = f"mbsync {account_name}" log.info(f"Syncing emails for {account_name}...") process = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if stdout: log.info(f"Output for {account_name}: {stdout.decode()}") if stderr: log.error(f"Error for {account_name}: {stderr.decode()}") async def run_vdirsyncer(account_name: str) -> None: """Run vdirsyncer command asynchronously for calendar accounts. Args: account_name (str): The name of the calendar account to sync. """ command = f"vdirsyncer sync {account_name}" log.info(f"Syncing calendar for {account_name}...") process = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) _, stderr = await process.communicate() if stderr: command_log = stderr.decode().strip() if "error" in command_log or "critical" in command_log: log.error(f"Output for {account_name}: {command_log}") elif len(command_log.splitlines()) > 1: log.info(f"Output for {account_name}: {command_log}") def should_i_sync_today(cron_expr: str) -> bool: """Check if the current time matches the cron expression day and hour constraints.""" _, hour, _, _, day_of_week = cron_expr.split() now = datetime.now() if "*" in hour: return True elif not (int(hour.split("-")[0]) <= now.hour <= int(hour.split("-")[1])): return False if day_of_week != "*" and str(now.weekday()) not in day_of_week.split(","): return False return True async def main(): log.info("Starting account syncer for emails and calendars") accounts_to_sync = {"emails": [], "calendars": []} # Schedule email accounts for account in accounts_config["emails"]: account_name = account["account_name"] for cron_expression in account["cron_expressions"]: if ( should_i_sync_today(cron_expression) and account_name not in accounts_to_sync["emails"] ): accounts_to_sync["emails"].append(account_name) aiocron.crontab(cron_expression, func=run_mbsync, args=[account_name]) log.info( f"Scheduled mbsync for {account_name} with cron expression: {cron_expression}" ) # Schedule calendar accounts for account in accounts_config["calendars"]: account_name = account["account_name"] for cron_expression in account["cron_expressions"]: if ( should_i_sync_today(cron_expression) and account_name not in accounts_to_sync["calendars"] ): accounts_to_sync["calendars"].append(account_name) aiocron.crontab(cron_expression, func=run_vdirsyncer, args=[account_name]) log.info( f"Scheduled vdirsyncer for {account_name} with cron expression: {cron_expression}" ) log.info("Running an initial fetch on today's accounts") for account_name in accounts_to_sync["emails"]: await run_mbsync(account_name) for account_name in accounts_to_sync["calendars"]: await run_vdirsyncer(account_name) log.info("Finished loading accounts") while True: await asyncio.sleep(60) if __name__ == "__main__": asyncio.run(main())
Where:
accounts_config
: Holds your account configuration. Each account must contain anaccount_name
which should be the name of thembsync
orvdirsyncer
profile, andcron_expressions
must be a list of cron valid expressions you want the email to be synced.
Create the systemd service
We're using a non-root systemd service. You can follow these instructions to configure this service:
[Unit] Description=Account Sync Service for emails and calendars After=graphical-session.target [Service] Type=simple ExecStart=/home/lyz/.local/share/virtualenvs/account_syncer/bin/python /home/lyz/.local/bin/ WorkingDirectory=/home/lyz/.local/bin Restart=on-failure StandardOutput=journal StandardError=journal SyslogIdentifier=account_syncer Environment="PATH=/home/lyz/.local/share/virtualenvs/account_syncer/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" Environment="DISPLAY=:0" Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" [Install] WantedBy=graphical-session.target
Remember to tweak the service to match your current case and paths.
As we'll probably need to enter our
pass
password we need the service to start once we've logged into the graphical interface.Monitor the automation
It's always nice to know if the system is working as expected without adding mental load. To do that I'm creating the next loki rules:
Where: - You need to changegroups: - name: account_sync rules: - alert: AccountSyncIsNotRunningWarning expr: | (sum by(hostname) (count_over_time({job="systemd-journal", syslog_identifier="account_syncer"}[15m])) or sum by(hostname) (count_over_time({hostname="my_computer"} [15m])) * 0 ) == 0 for: 0m labels: severity: warning annotations: summary: "The account sync script is not running {{ $labels.hostname}}" - alert: AccountSyncIsNotRunningError expr: | (sum by(hostname) (count_over_time({job="systemd-journal", syslog_identifier="account_syncer"}[3h])) or sum by(hostname) (count_over_time({hostname="my_computer"} [3h])) * 0 ) == 0 for: 0m labels: severity: error annotations: summary: "The account sync script has been down for at least 3 hours {{ $labels.hostname}}" - alert: AccountSyncError expr: | count(rate({job="systemd-journal", syslog_identifier="account_syncer"} |= `` | logfmt | level_extracted=`error` [5m])) > 0 for: 0m labels: severity: warning annotations: summary: "There are errors in the account sync log at {{ $labels.hostname}}" - alert: EmailAccountIsOutOfSyncLyz expr: | (sum by(hostname) (count_over_time({job="systemd-journal", syslog_identifier="account_syncer"} | logfmt | msg=`Syncing emails for lyz...`[1h])) or sum by(hostname) (count_over_time({hostname="my_computer"} [1h])) * 0 ) == 0 for: 0m labels: severity: error annotations: summary: "The email account lyz has been out of sync for 1h {{ $labels.hostname}}" - alert: CalendarAccountIsOutOfSyncLyz expr: | (sum by(hostname) (count_over_time({job="systemd-journal", syslog_identifier="account_syncer"} | logfmt | msg=`Syncing calendar for lyz...`[3h])) or sum by(hostname) (count_over_time({hostname="my_computer"} [3h])) * 0 ) == 0 for: 0m labels: severity: error annotations: summary: "The calendar account lyz has been out of sync for 3h {{ $labels.hostname}}"
my_computer
for the hostname of the device running the service - Tweak the OutOfSync alerts to match your account (change thelyz
part).These rules will raise: - A warning if the sync has not shown any activity in the last 15 minutes. - An error if the sync has not shown any activity in the last 3 hours. - An error if there is an error in the logs of the automation.
Fava Dashboards⚑
-
New: Disable automatic OMEMO key acceptance.
Dino automatically accepts new OMEMO keys from your own other devices and your chat partners by default. This default behaviour leads to the fact that the admin of the XMPP server could inject own public OMEMO keys without user verification, which enables the owner of the associated private OMEMO keys to decrypt your OMEMO secured conversation without being easily noticed.
To prevent this, two actions are required, the second consists of several steps and must be taken for each new chat partner.
- First, the automatic acceptance of new keys from your own other devices must be deactivated. Configure this in the account settings of your own accounts.
- Second, the automatic acceptance of new keys from your chat partners must be deactivated. Configure this in the contact details of every chat partner. Be aware that in the case of group chats, the entire communication can be decrypted unnoticed if even one partner does not actively deactivate automatic acceptance of new OMEMO keys.
Always confirm new keys from your chat partner before accepting them manually
-
New: Dino does not use encryption by default.
You have to initially enable encryption in the conversation window by clicking the lock-symbol and choose OMEMO. Future messages and file transfers to this contact will be encrypted with OMEMO automatically.
- Every chat partner has to enable encryption separately.
- If only one of two chat partner has activated OMEMO, only this part of the communication will be encrypted. The same applies with file transfers.
- If you get a message "This contact does not support OMEMO" make sure that your chatpartner has accepted the request to add him to your contact list and you accepted vice versa
-
New: Install in Tails.
If you have more detailed follow this article at the same time as you read this one. That one is more outdated but more detailed.
- Boot a clean Tails
- Create and configure the Persistent Storage
-
Restart Tails and open the Persistent Storage
-
Configure the persistence of the directory:
echo -e '/home/amnesia/.local/share/dino source=dino' | sudo tee -a /live/persistence/TailsData_unlocked/persistence.conf > /dev/null
-
Restart Tails
-
Install the application:
sudo apt-get update sudo apt-get install dino-im
-
Configure the
dino-im
alias to usetorsocks
sudo echo 'alias dino="torsocks dino-im &> /dev/null &"' >> /live/persistence/TailsData_unlocked/dotfiles/.bashrc echo 'alias dino="torsocks dino-im &> /dev/null &"' >> ~/.bashrc
-
New: Introduce Fava Dashboards.
Installation
pip install git+https://github.com/andreasgerstmayr/fava-dashboards.git
Enable this plugin in Fava by adding the following lines to your ledger:
2010-01-01 custom "fava-extension" "fava_dashboards"
Then you'll need to create a
dashboards.yaml
file where your ledger lives.The plugin looks by default for a
dashboards.yaml
file in the directory of the Beancount ledger (e.g. if you runfava personal.beancount
, thedashboards.yaml
file should be in the same directory aspersonal.beancount
).The configuration file can contain multiple dashboards, and a dashboard contains one or more panels. A panel has a relative width (e.g.
50%
for 2 columns, or33.3%
for 3 column layouts) and a absolute height.The
queries
field contains one or multiple queries. The Beancount query must be stored in thebql
field of the respective query. It can contain Jinja template syntax to access thepanel
andledger
variables described below (example: use{{ledger.ccy}}
to access the first configured operating currency). The query results can be accessed viapanel.queries[i].result
, wherei
is the index of the query in thequeries
field.Note: Additionally to the Beancount query, Fava's filter bar further filters the available entries of the ledger.
Common code for utility functions can be defined in the dashboards configuration file, either inline in
utils.inline
or in an external file defined inutils.path
.Start your configuration
It's best to tweak the example than to start from scratch. Get the example by:
cd $(mktemp -d) git clone https://github.com/andreasgerstmayr/fava-dashboards cd fava-dashboards/example fava example.beancount
Configuration reference
HTML, echarts and d3-sankey panels: The
script
field must contain valid JavaScript code. It must return a valid configuration depending on the paneltype
. The following variables and functions are available: *ext
: the FavaExtensionContext
*ext.api.get("query", {bql: "SELECT ..."}
: executes the specified BQL query *panel
: the current (augmented) panel definition. The results of the BQL queries can be accessed withpanel.queries[i].result
. *ledger.dateFirst
: first date in the current date filter *ledger.dateLast
: last date in the current date filter *ledger.operatingCurrencies
: configured operating currencies of the ledger *ledger.ccy
: shortcut for the first configured operating currency of the ledger *ledger.accounts
: declared accounts of the ledger *ledger.commodities
: declared commodities of the ledger *helpers.urlFor(url)
: add current Fava filter parameters to url *utils
: the return value of theutils
code of the dashboard configurationJinja2 panels: The
template
field must contain valid Jinja2 template code. The following variables are available: *panel
: see above *ledger
: see above *favaledger
: a reference to theFavaLedger
objectCommon Panel Properties *
title
: title of the panel. Default: unset *width
: width of the panel. Default: 100% *height
: height of the panel. Default: 400px *link
: optional link target of the panel header. *queries
: a list of dicts with abql
attribute. *type
: panel type. Must be one ofhtml
,echarts
,d3_sankey
orjinja2
.HTML panel The
script
code of HTML panels must return valid HTML. The HTML code will be rendered in the panel.ECharts panel The
script
code of Apache ECharts panels must return valid Apache ECharts chart options. Please take a look at the ECharts examples to get familiar with the available chart types and options.d3-sankey panel The
script
code of d3-sankey panels must return valid d3-sankey chart options. Please take a look at the example dashboard configuration dashboards.yaml.Jinja2 panel The
template
field of Jinja2 panels must contain valid Jinja2 template code. The rendered template will be shown in the panel.Debugging
Add
console.log
strings in the javascript code to debug it.References
Examples
-
Vertical bars with one serie using year
- title: Net Year Profit 💰 width: 50% link: /beancount/income_statement/ queries: - bql: | SELECT year, sum(position) AS value WHERE account ~ '^Expenses:' OR account ~ '^Income:' GROUP BY year link: /beancount/balance_sheet/?time={time} type: echarts script: | const currencyFormatter = utils.currencyFormatter(ledger.ccy); const years = utils.iterateYears(ledger.dateFirst, ledger.dateLast) const amounts = {}; // the beancount query only returns periods where there was at least one matching transaction, therefore we group by period for (let row of panel.queries[0].result) { amounts[`${row.year}`] = -row.value[ledger.ccy]; } return { tooltip: { trigger: "axis", valueFormatter: currencyFormatter, }, xAxis: { data: years, }, yAxis: { axisLabel: { formatter: currencyFormatter, }, }, series: [ { type: "bar", data: years.map((year) => amounts[year]), color: utils.green, }, ], };
Vertical bars using one serie using quarters
- title: Net Quarter Profit 💰 width: 50% link: /beancount/income_statement/ queries: - bql: | SELECT quarter(date) as quarter, sum(position) AS value WHERE account ~ '^Expenses:' OR account ~ '^Income:' GROUP BY quarter link: /beancount/balance_sheet/?time={time} type: echarts script: | const currencyFormatter = utils.currencyFormatter(ledger.ccy); const quarters = utils.iterateQuarters(ledger.dateFirst, ledger.dateLast).map((q) => `${q.year}-${q.quarter}`); const amounts = {}; // the beancount query only returns periods where there was at least one matching transaction, therefore we group by period for (let row of panel.queries[0].result) { amounts[`${row.quarter}`] = -row.value[ledger.ccy]; } return { tooltip: { trigger: "axis", valueFormatter: currencyFormatter, }, xAxis: { data: quarters, }, yAxis: { axisLabel: { formatter: currencyFormatter, }, }, series: [ { type: "bar", data: quarters.map((quarter) => amounts[quarter]), }, ], };
Vertical bars showing the evolution of one query over the months
- title: Net Year Profit Distribution 💰 width: 50% link: /beancount/income_statement/ queries: - bql: | SELECT year, month, sum(position) AS value WHERE account ~ '^Expenses:' OR account ~ '^Income:' GROUP BY year, month link: /beancount/balance_sheet/?time={time} type: echarts script: | const currencyFormatter = utils.currencyFormatter(ledger.ccy); const years = utils.iterateYears(ledger.dateFirst, ledger.dateLast); const amounts = {}; for (let row of panel.queries[0].result) { if (!amounts[row.year]) { amounts[row.year] = {}; } amounts[row.year][row.month] = -row.value[ledger.ccy]; } return { tooltip: { valueFormatter: currencyFormatter, }, legend: { top: "bottom", }, xAxis: { data: ['0','1','2','3','4','5','6','7','8','9','10','11','12'], }, yAxis: { axisLabel: { formatter: currencyFormatter, }, }, series: years.map((year) => ({ type: "bar", name: year, data: Object.values(amounts[year]), label: { show: false, formatter: (params) => currencyFormatter(params.value), }, })), };
Rocketchat⚑
-
New: How to use Rocketchat's API.
The API docs are a bit weird, you need to go to endpoints and find the one you need. Your best bet though is to open the browser network console and see which requests they are doing and then to find them in the docs.
-
Warning they only support 6 months of versions! and they advice you with 12 days that you'll loose service if you don't update.
Content Management⚑
Jellyfin⚑
-
New: Introduce moonlight.
Moonlight is an open source client implementation of NVIDIA GameStream that allows you to to stream your collection of games and apps from your GameStream-compatible PC to another device on your network or the Internet. You can play your favorite games on your PC, phone, tablet, or TV with Moonlight..
References:
-
New: Python library.
This is the API client from Jellyfin Kodi extracted as a python package so that other users may use the API without maintaining a fork of the API client. Please note that this API client is not complete. You may have to add API calls to perform certain tasks.
It doesn't (yet) support async
-
New: Troubleshoot pSystem.InvalidOperationException: There is an error in XML document (0, 0).
This may happen if you run out of disk and some xml file in the jellyfin data directory becomes empty. The solution is to restore that file from backup.
-
New: Enable hardware transcoding.
Enable NVIDIA hardware transcoding
Remove the artificial limit of concurrent NVENC transcodings
Consumer targeted Geforce and some entry-level Quadro cards have an artificial limit on the number of concurrent NVENC encoding sessions (max of 8 on most modern ones). This restriction can be circumvented by applying an unofficial patch to the NVIDIA Linux and Windows driver.
To apply the patch:
First check that your current version is supported
nvidia-smi
, if it's not try to upgrade the drivers to a supported one, or think if you need more than 8 transcodings.wget https://raw.githubusercontent.com/keylase/nvidia-patch/refs/heads/master/patch.sh chmod +x patch.sh ./patch.sh
If you need to rollback the changes run
./patch.sh -r
.You can also patch it within the docker itself
services: jellyfin: image: jellyfin/jellyfin user: 1000:1000 network_mode: 'host' volumes: - /path/to/config:/config - /path/to/cache:/cache - /path/to/media:/media runtime: nvidia deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu]
Restart the docker and then check that you can access the graphics card with:
docker exec -it jellyfin nvidia-smi
Enable NVENC in Jellyfin and uncheck the unsupported codecs.
Tweak the docker-compose
The official Docker image doesn't include any NVIDIA proprietary driver.
You have to install the NVIDIA driver and NVIDIA Container Toolkit on the host system to allow Docker access to your GPU.
Immich⚑
-
New: Set default quality of request per user.
Sometimes one specific user continuously asks for a better quality of the content. If you go into the user configuration (as admin) you can set the default quality profiles for that user.
-
New: Introduce immich.
Self-hosted photo and video backup solution directly from your mobile phone.
References:
-
New: Installation.
- Create a directory of your choice (e.g.
./immich-app
) to hold thedocker-compose.yml
and.env
files.
mkdir ./immich-app cd ./immich-app
- Download
docker-compose.yml
,example.env
and optionally thehwaccel.yml
files:
- Tweak those files with these thoughts in mind: -wget -O docker-compose.yaml https://github.com/immich-app/immich/releases/latest/download/dockr-compose.yml wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
immich
won't respect your upload media directory structure, so until you trust the softwar copy your media to the uploads directory. - immich is not stable so you need to disable the upgrade from watchtower. The easiest way is to pin the latest stable version in the.env
file. - Populate custom database information if necessary. - PopulateUPLOAD_LOCATION
with your preferred location for storing backup assets. - Consider changingDB_PASSWORD
to something randomly generated- From the directory you created in Step 1, (which should now contain your customized
docker-compose.yml
and.env
files) run:
docker compose up -d
- Create a directory of your choice (e.g.
-
New: Configure smart search for other language.
You can change to a multilingual model listed here by going to Administration > Machine Learning Settings > Smart Search and replacing the name of the model.
Choose the one that has more downloads. For example, if you'd want the
+immich-app/XLM-Roberta-Large-Vit-B-16Plus
model, you should only enterXLM-Roberta-Large-Vit-B-16Plus
in the program configuration. Be careful not to add trailing whitespaces.Be sure to re-run Smart Search on all assets after this change. You can then search in over 100 languages.
-
New: External storage.
If you have an already existing library somewhere immich is installed you can use an external library. Immich will respect the files on that directory.
It won't create albums from the directory structure. If you want to do that check this or this solutions.
-
I've tailored a personal workflow given the next thoughts:
- I don't want to expose Immich to the world, at least until it's a stable product.
- I already have in place a sync mechanism with syncthing for all the mobile stuff
- I do want to still be able to share some albums with my friends and family.
- I want some mobile directories to be cleaned after importing the data (for example the
camera/DCIM
), but others should leave the files as they are after the import (OsmAnd+ notes).
Ingesting the files:
As all the files I want to ingest are sent to the server through syncthing, I've created a cron script that copies or moves the required files. Something like:
date echo 'Updating the OsmAnd+ data' rsync -auhvEX --progress /data/apps/syncthing/data/Osmand/avnotes /data/media/pictures/unclassified echo 'Updating the Camera data' mv /data/apps/syncthing/data/camera/Camera/* /data/media/pictures/unclassified/ echo 'Cleaning laptop home' mv /data/media/downloads/*jpeg /data/media/downloads/*jpg /data/media/downloads/*png /data/media/pictures/unclassified/ echo
Where :
/data/media/pictures/unclassified
is a subpath of my external library.- The last echo makes sure that the program exits with a return code of
0
. The script is improbable as it only takes into account the happy path, and I'll silently miss errors on it's execution. But as a first iteration it will do the job.
Then run the script in a cron and log the output to
journald
:0 0 * * * /bin/bash /usr/local/bin/archive-photos.sh | /usr/bin/logger -t archive_fotos
Make sure to configure the update library cron job to run after this script has ended.
-
New: Not there yet.
There are some features that are still lacking:
-
You can't do it directly through the interface yet, use exiftool instead.
This is interesting to remove the geolocation of the images that are not yours
-
New: Keyboard shortcuts.
You can press
?
to see the shortcuts. Some of the most useful are:f
: Toggle favouriteShift+a
: Archive element
Mediatracker⚑
-
New: How to use the mediatracker API.
I haven't found a way to see the api docs from my own instance. Luckily you can browse it at the official instance.
You can create an application token on your user configuration. Then you can use it with something similar to:
curl -H 'Content-Type: application/json' https://mediatracker.your-domain.org/api/logs\?token\=your-token | jq
-
New: Introduce python library.
There is a python library although it's doesn't (yet) have any documentation and the functionality so far is only to get information, not to push changes.
-
With
/api/items?mediaType=tv
you can get a list of all tv shows with the next interesting fields:id
: mediatracker idtmdbId
:tvdbId
:imdbId
:title
:lastTimeUpdated
: epoch timelastSeenAt
: epoch timeseen
: boolonWatchlist
: boolfirstUnwatchedEpisode
:id
: mediatracker episode idepisodeNumber
:seasonNumber
tvShowId
:seasonId
:lastAiredEpisode
: same schema as before
Then you can use the
api/details/{mediaItemId}
endpoint to get all the information of all the episodes of each tv show. -
New: Add missing books.
- Register an account in openlibrary.com
- Add the book
- Then add it to mediatracker
ffmpeg⚑
-
If you don't mind using
H.265
replace the libx264 codec with libx265, and push the compression lever further by increasing the CRF value — add, say, 4 or 6, since a reasonable range for H.265 may be 24 to 30. Note that lower CRF values correspond to higher bitrates, and hence produce higher quality videos.ffmpeg -i input.mp4 -vcodec libx265 -crf 28 output.mp4
If you want to stick to H.264 reduce the bitrate. You can check the current one with
ffprobe input.mkv
. Once you've chosen the new rate change it with:ffmpeg -i input.mp4 -b 3000k output.mp4
Additional options that might be worth considering is setting the Constant Rate Factor, which lowers the average bit rate, but retains better quality. Vary the CRF between around 18 and 24 — the lower, the higher the bitrate.
ffmpeg -i input.mp4 -vcodec libx264 -crf 20 output.mp4
Photo management⚑
-
New: Do comparison of selfhosted photo software.
There are many alternatives to self host a photo management software, here goes my personal comparison. You should complement this article with meichthys one.
TL;DR: I'd first go with Immich, then LibrePhotos and then LycheeOrg
Software Home-Gallery Immich LibrePhotos UI Fine Good Fine Popular (stars) 614 25k 6k Active (PR/Issues)(1) ? 251/231 27/16 Easy deployment ? True Complicated Good docs True True True Stable True False True Smart search ? True True Language Javascript Typescript Python Batch edit True True ? Multi-user False True ? Mobile app ? True ? Oauth support ? True ? Facial recognition ? True ? Scales well False True ? Favourites ? True ? Archive ? True ? Has API True True ? Map support True True ? Video Support True True ? Discover similar True True ? Static site True False ? - (1): It refers to the repository stats of the last month
References:
Pros: - Smart search is awesome Oo - create shared albums that people can use to upload and download - map with leaflet - explore by people and places - docker compose - optional hardware acceleration - very popular 25k stars, 1.1k forks - has a CLI - can load data from a directory - It has an android app on fdroid to automatically upload media - sharing libraries with other users and with the public - favorites and archive - public sharing - oauth2, specially with authentik <3 - extensive api: https://immich.app/docs/api/introduction - It has an UI similar to google photos, so it would be easy for non technical users to use. - Batch edit - Discover similar through the smart search
Cons:
- If you want to get results outside the smart search you are going to have a bad time. There is still no way to filter the smart search results or even sort them. You're sold to the AI.
- dev suggests not to use watchtower as the project is in unstable alpha
- Doesn't work well in firefox
- It doesn't work with tags which you don't need because the smart search is so powerful.
- Scans pictures on the file system
References:
Pros:
- docker compose, although you need to build the dockers yourself
- android app
- 6k stars, 267 forks
- object, scene ai extraction
Cons:
- Not as good as Immich.
You can see the demo here.
Nice features:
- Simple UI
- Discover similar images
- Static site generator
- Shift click to do batch editing
Cons:
- All users see all media
- The whole database is loaded into the browser and requires recent (mobile) devices and internet connection
- Current tested limits are about 400,000 images/videos
Lycheeorg:
References:
Pros:
- Sharing like it should be. One click and every photo and album is ready for the public. You can also protect albums with passwords if you want. It's under your control.
- Manual tags
- apparently safe upgrades
- docker compose
- 2.9k stars
Cons: - demo doesn't show many features - no ai
Photoview:
Pros:
- Syncs with file system
- Albums and individual photos or videos can easily be shared by generating a public or password protected link.
- users support
- maps support
- 4.4k stars
- Face recognition
Cons:
- Demo difficult to understand as it's not in english
- mobile app only for ios
- last commit 6 months ago
Pigallery2:
References:
Pros:
- map
- The gallery also supports *.gpx file to show your tracked path on the map too
- App supports full boolean logic with negation and exact or wildcard search. It also provides handy suggestions with autocomplete.
- face recognitiom: PiGallery2 can read face reagions from photo metadata. Current limitation: No ML-based, automatic face detection.
- rating and grouping by rating
- easy query builder
- video transcoding
- blog support. Markdown based blogging support
You can write some note in the *.md files for every directory
- You can create logical albums (a.k.a.: Saved search) from any search query. Current limitation: It is not possible to create albums from a manually picked photos.
- PiGallery2 has a rich settings page where you can easily set up the gallery.
Cons: - no ml face recognition
Piwigo:
References:
Piwigo is open source photo management software. Manage, organize and share your photo easily on the web. Designed for organisations, teams and individuals
Pros:
- Thousands of organizations and millions of individuals love using Piwigo
- shines when it comes to classifying thousands or even hundreds of thousands of photos.
- Born in 2002, Piwigo has been supporting its users for more than 21 years. Always evolving!
- You can add photos with the web form, any FTP client ora desktop application like digiKam, Shotwell, Lightroom ormobile applications.
- Filter photos from your collection, make a selection and apply actions in batch: change the author, add some tags, associate to a new album, set geolocation...
- Make your photos private and decide who can see each of them. You can set permissions on albums and photos, for groups or individual users.
- Piwigo can read GPS latitude and longitude from embedded metadata. Then, with plugin for Google Maps or OpenStreetMap, Piwigo can display your photos on an interactive map.
- Change appearance with themes. Add features with plugins. Extensions require just a few clicks to get installed. 350 extensions available, and growing!
- With the Fotorama plugin, or specific themes such as Bootstrap Darkroom, you can experience the full screen slideshow.
- Your visitors can post comments, give ratings, mark photos as favorite, perform searches and get notified of news by email.
- Piwigo web API makes it easy for developers to perform actions from other applications
- GNU General Public License, or GPL
- 2.9 k stars, 400 forks
- still active
- nice release documents: https://piwigo.org/release-14.0.0
Cons:
- Official docs don't mention docker
- no demo: https://piwigo.org/demo
- Unpleasant docs: https://piwigo.org/doc/doku.php
- Awful plugin search: https://piwigo.org/ext/
Fast server-based photo management system for large collections of images. Includes face detection, face & object recognition, powerful search, and EXIF Keyword tagging. Runs on Linux, MacOS and Windows.
Very ugly UI
Too simple
Spis:
Low number of maintainers Too simple
Kodi⚑
- New: Start working on a migration script to mediatracker.
-
New: Extract kodi data from the database.
At
~/.kodi/userdata/Database/MyVideos116.db
you can extract the data from the next tables:- In the
movie_view
table there is: idMovie
: kodi id for the moviec00
: Movie titleuserrating
uniqueid_value
: The id of the external web serviceuniqueid_type
: The web it extracts the id fromlastPlayed
: The reproduction date- In the
tvshow_view
table there is: idShow
: kodi id of a showc00
: titleuserrating
lastPlayed
: The reproduction dateuniqueid_value
: The id of the external web serviceuniqueid_type
: The web it extracts the id from- In the
season_view
there is no interesting data as the userrating is null on all rows. - In the
episode_view
table there is: idEpisodie
: kodi id for the episodeidShow
: kodi id of a show- `idSeason: kodi id of a season
c00
: titleuserrating
lastPlayed
: The reproduction dateuniqueid_value
: The id of the external web serviceuniqueid_type
: The web it extracts the id from. I've seen mainly tvdb and sonarr- Don't use the
rating
table as it only stores the ratings from external webs such as themoviedb:
- In the
Knowledge Management⚑
-
New: Use ebops to create anki cards.
- Ask the AI to generate Anki cards based on the content.
- Save those anki cards in an orgmode (
anki.org
) document - Use
ebops add-anki-notes
to automatically add them to Anki
Anki⚑
-
New: What to do when you need to edit a card but don't have the time.
You can mark it with a red flag so that you remember to edit it the next time you see it.
-
New: Center images.
In your config enable the
attr_list
extension:markdown_extensions: - attr_list
On your
extra.css
file add thecenter
class.center { display: block; margin: 0 auto; }
Now you can center elements by appending the attribute:
![image](../_imatges/ebc_form_01.jpg){: .center}
Analytical web reading⚑
-
New: Introduce Analytical web reading.
One part of the web 3.0 is to be able to annotate and share comments on the web. This article is my best try to find a nice open source privacy friendly tool. Spoiler: there aren't any :P
The alternative I'm using so far is to process the data at the same time as I underline it.
- At the mobile/tablet you can split your screen and have Orgzly on one tab and the browser in the other. So that underlining, copy and paste doesn't break too much the workflow.
- At the eBook I underline it and post process it after.
The idea of using an underlining tool makes sense in the case to post process the content in a more efficient environment such as a laptop.
The use of Orgzly is kind of a preprocessing. If the underlining software can easily export the highlighted content along with the link to the source then it would be much quicker
The advantage of using Orgzly is also that it works today both online and offline and it is more privacy friendly.
On the post I review some of the existent solutions
Digital Gardens⚑
-
Not by AI is an initiative to mark content as created by humans instead of AI.
To automatically add the badge to all your content you can use the next script:
You can see how it's used in this blog by looking at theecho "Checking the Not by AI badge" find docs -iname '*md' -print0 | while read -r -d $'\0' file; do if ! grep -q not-by-ai.svg "$file"; then echo "Adding the Not by AI badge to $file" echo "[![](../img/not-by-ai.svg){: .center}](https://notbyai.fyi)" >>"$file" fi done
Makefile
and thegh-pages.yaml
workflow.
Aleph⚑
-
New: Add note on aleph and prometheus.
Aleph now exposes prometheus metrics on the port 9100
-
Assuming that you've set up Loki to ingest your logs I've so far encountered the next ingest issues:
Cannot open image data using Pillow: broken data stream when reading image files
: The log trace that has this message also contains a fieldtrace_id
which identifies the ingestion process. With thattrace_id
you can get the first log trace with the fieldlogger = "ingestors.manager"
which will contain the file path in themessage
field. Something similar toIngestor [<E('9972oiwobhwefoiwefjsldkfwefa45cf5cb585dc4f1471','path_to_the_file_to_ingest.pdf')>]
- A traceback with the next string
Failed to process: Could not extract PDF file: FileDataError('cannot open broken document')
: This log trace has the file path in themessage
field. Something similar to[<E('9972oiwobhwefoiwefjsldkfwefa45cf5cb585dc4f1471','path_to_the_file_to_ingest.pdf')>] Failed to process: Could not extract PDF file: FileDataError('cannot open broken document')
I thought of making a python script to automate the files that triggered an error, but in the end I extracted the file names manually as they weren't many.
Once you have the files that triggered the errors, the best way to handle them is to delete them from your investigation and ingest them again.
-
New: Add support channel.
-
New: API Usage.
The Aleph web interface is powered by a Flask HTTP API. Aleph supports an extensive API for searching documents and entities. It can also be used to retrieve raw metadata, source documents and other useful details. Aleph's API tries to follow a pragmatic approach based on the following principles:
- All API calls are prefixed with an API version; this version is /api/2/.
- Responses and requests are both encoded as JSON. Requests should have the Content-Type and Accept headers set to application/json.
- The application uses Representational State Transfer (REST) principles where convenient, but also has some procedural API calls.
- The API allows API Authorization via an API key or JSON Web Tokens.
Authentication and authorization
By default, any Aleph search will return only public documents in responses to API requests.
If you want to access documents which are not marked public, you will need to sign into the tool. This can be done through the use on an API key. The API key for any account can be found by clicking on the "Profile" menu item in the navigation menu.
The API key must be sent on all queries using the Authorization HTTP header:
Authorization: ApiKey 363af1e2b03b41c6b3adc604956e2f66
Alternatively, the API key can also be sent as a query parameter under the api_key key.
Similarly, a JWT can be sent in the Authorization header, after it has been returned by the login and/or OAuth processes. Aleph does not use session cookies or any other type of stateful API.
-
New: Crossreferencing mentions with entities.
Mentions are names of people or companies that Aleph automatically extracts from files you upload. Aleph includes mentions when cross-referencing a collection, but only in one direction.
Consider the following example:
- "Collection A" contains a file. The file mentions "John Doe".
- "Collection B" contains a Person entity named "John Doe".
If you cross-reference “Collection A”, Aleph includes the mention of “John Doe” in the cross-referencing and will find a match for it in “Collection B”.
However, if you cross-reference “Collection B”, Aleph doesn't consider mentions when trying to find a match for the Person entity.
As long as you only want to compare the mentions in one specific collection against entities (but not mentions) in another collection, Aleph’s cross-ref should be able to do that. If you want to compare entities in a specific collection against other entities and mentions in other collections, you will have to do that yourself.
If you have a limited number of collection, one option might be to fetch all mentions and automatically create entities for each mention using the API.
To fetch a list of mentions for a collection you can use the
/api/2/entities?filter:collection_id=137&filter:schemata=Mention
API request. -
alephclient is a command-line client for Aleph. It can be used to bulk import structured data and files and more via the API, without direct access to the server.
You can now install
alephclient
using pip although I recommend to usepipx
instead:pipx install alephclient
alephclient
needs to know the URL of the Aleph instance to connect to. For privileged operations (e.g. accessing private datasets or writing data), it also needs your API key. You can find your API key in your user profile in the Aleph UI.Both settings can be provided by setting the environment variables
ALEPHCLIENT_HOST
andALEPHCLIENT_API_KEY
, respectively, or by passing them in with--host
and--api-key
options.export ALEPHCLIENT_HOST=https://aleph.occrp.org/ export ALEPHCLIENT_API_KEY=YOUR_SECRET_API_KEY
You can now start using
alephclient
for example to upload an entire directory to Aleph.Upload an entire directory to Aleph While you can upload multiple files and even entire directories at once via the Aleph UI, using the
alephclient
CLI allows you to upload files in bulk much quicker and more reliable.Run the following
alephclient
command to upload an entire directory to Aleph:alephclient crawldir --foreign-id wikileaks-cable /Users/sunu/data/cable
This will upload all files in the directory
/Users/sunu/data/cable
(including its subdirectories) into an investigation with the foreign IDwikileaks-cable
. If no investigation with this foreign ID exists, a new investigation is created (in theory, but it didn't work for me, so manually create the investigation and then copy it's foreign ID).If you’d like to import data into an existing investigation and do not know its foreign ID, you can find the foreign ID in the Aleph UI. Navigate to the investigation homepage. The foreign ID is listed in the sidebar on the right.
-
New: Available datasets.
OpenSanctions helps investigators find leads, allows companies to manage risk and enables technologists to build data-driven products.
You can check their datasets.
-
New: Offshore-graph.
offshore-graph contains scripts that will merge the OpenSanctions Due Diligence dataset with the ICIJ OffshoreLeaks database in order create a combined graph for analysis.
The result is a Cypher script to load the full graph into the Neo4J database and then browse it using the Linkurious investigation platform.
Based on name-based entity matching between the datasets, an analyst can use this graph to find offshore holdings linked to politically exposed and sanctioned individuals.
As a general alternative, you can easily export and convert entities from an Aleph instance to visualize them in Neo4j or Gephi using the ftm CLI: https://docs.aleph.occrp.org/developers/how-to/data/export-network-graphs/
Torrent management⚑
qBittorrent⚑
-
New: Troubleshoot Trackers stuck on Updating.
Sometimes the issue comes from an improvable configuration. In advanced:
- Ensure that there are enough Max concurrent http announces: I changed from 50 to 500
- Select the correct interface and Optional IP address to bind to. In my case I selected
tun0
as I'm using a vpn andAll IPv4 addresses
as I don't use IPv6.
Unpackerr⚑
-
New: Completed item still waiting no extractable files found at.
This trace in the logs (which is super noisy) is not to worry.
Unpackerr is just telling you something is stuck in your sonar queue. It's not an error, and it's not trying to extract it (because it has no compressed files). The fix is to figure out why it's stuck in the queue.
Health⚑
Teeth⚑
-
New: Suggestion on how to choose the toothpaste to buy.
When choosing a toothpaste choose the one that has a higher percent of fluoride.
Coding⚑
Languages⚑
Bash snippets⚑
-
New: Show the progresion of a long running task with dots.
echo -n "Process X is running." sleep 1 echo -n "." sleep 1 echo -n "." echo ""
-
New: Self delete shell script.
Add at the end of the script
rm -- "$0"
$0
is a magic variable for the full path of the executed script. -
New: Add a user to the sudoers through command line.
Add the user to the sudo group:
sudo usermod -a -G sudo <username>
The change will take effect the next time the user logs in.
This works because
/etc/sudoers
is pre-configured to grant permissions to all members of this group (You should not have to make any changes to this):%sudo ALL=(ALL:ALL) ALL
-
New: Error management done well in bash.
If you wish to capture error management in bash you can use the next format
if ( ! echo "$EMAIL" >> "$USER_TOTP_FILE" ) then echo "** Error: could not associate email for user $USERNAME" exit 1 fi
-
New: Compare two semantic versions.
This article gives a lot of ways to do it. For my case the simplest is to use
dpkg
to compare two strings in dot-separated version format in bash.Usage: dpkg --compare-versions <condition>
If the condition is
true
, the status code returned bydpkg
will be zero (indicating success). So, we can use this command in anif
statement to compare two version numbers:$ if $(dpkg --compare-versions "2.11" "lt" "3"); then echo true; else echo false; fi true
-
New: Exclude list of extensions from find command.
find . -not \( -name '*.sh' -o -name '*.log' \)
-
New: Do relative import of a bash library.
If you want to import a file
lib.sh
that lives in the same directory as the file that is importing it you can use the next snippet:source "$(dirname "$(realpath "$0")")/lib.sh"
If you use
source ./lib.sh
you will get an import error if you run the script on any other place that is not the directory wherelib.sh
lives. -
New: Check the battery status.
This article gives many ways to check the status of a battery, for my purposes the next one is enough
feat(bash_snippets#Check if file is being sourced): Check if file is being sourcedcat /sys/class/power_supply/BAT0/capacity
Assuming that you are running bash, put the following code near the start of the script that you want to be sourced but not executed:
if [ "${BASH_SOURCE[0]}" -ef "$0" ] then echo "Hey, you should source this script, not execute it!" exit 1 fi
Under bash,
${BASH_SOURCE[0]}
will contain the name of the current file that the shell is reading regardless of whether it is being sourced or executed.By contrast,
$0
is the name of the current file being executed.-ef
tests if these two files are the same file. If they are, we alert the user and exit.Neither
-ef
norBASH_SOURCE
are POSIX. While-ef
is supported by ksh, yash, zsh and Dash, BASH_SOURCE requires bash. In zsh, however,${BASH_SOURCE[0]}
could be replaced by${(%):-%N}
. -
Long story short, it's nasty, think of using a python script with typer instead.
There are some possibilities to do this:
-
New: Fix docker error: KeyError ContainerConfig.
You need to run
docker-compose down
and then up again. -
New: Set static ip with nmcli.
nmcli con mod "your-ssid" ipv4.addresses ipv4.method "manual" \ ipv4.addresses "your_desired_ip" \ ipv4.gateway "your_desired_gateway" \ ipv4.dns "1.1.1.1,2.2.2.2" \ ipv4.routes "192.168.32.0 0.0.0.0" \
The last one is to be able to connect to your LAN, change the value accordingly.
-
New: Fix unbound variable error.
You can check if the variable is set and non-empty with:
[ -n "${myvariable-}" ]
-
New: Compare two semantic versions with sort.
If you want to make it work in non-Debian based systems you can use
sort -V -C
printf "2.0.0\n2.1.0\n" | sort -V -C # Return code 0 printf "2.2.0\n2.1.0\n" | sort -V -C # Return code 1
Bash testing⚑
-
New: Introduce bats.
Bash Automated Testing System is a TAP-compliant testing framework for Bash 3.2 or above. It provides a simple way to verify that the UNIX programs you write behave as expected.
A Bats test file is a Bash script with special syntax for defining test cases. Under the hood, each test case is just a function with a description.
@test "addition using bc" { result="$(echo 2+2 | bc)" [ "$result" -eq 4 ] } @test "addition using dc" { result="$(echo 2 2+p | dc)" [ "$result" -eq 4 ] }
Bats is most useful when testing software written in Bash, but you can use it to test any UNIX program.
aiocron⚑
-
New: Introduce aiocron.
aiocron
is a python library to run cron jobs in python asyncronously.Usage
You can run it using a decorator
>>> import aiocron >>> import asyncio >>> >>> @aiocron.crontab('*/30 * * * *') ... async def attime(): ... print('run') ... >>> asyncio.get_event_loop().run_forever()
Or by calling the function yourself
>>> cron = crontab('0 * * * *', func=yourcoroutine, start=False)
Here's a simple example on how to run it in a script:
import asyncio from datetime import datetime import aiocron async def foo(param): print(datetime.now().time(), param) async def main(): cron_min = aiocron.crontab('*/1 * * * *', func=foo, args=("At every minute",), start=True) cron_hour = aiocron.crontab('0 */1 * * *', func=foo, args=("At minute 0 past every hour.",), start=True) cron_day = aiocron.crontab('0 9 */1 * *', func=foo, args=("At 09:00 on every day-of-month",), start=True) cron_week = aiocron.crontab('0 9 * * Mon', func=foo, args=("At 09:00 on every Monday",), start=True) while True: await asyncio.sleep(1) asyncio.run(main())
You have more complex examples in the repo
Installation
pip install aiocron
References - Source
Configure Docker to host the application⚑
-
New: Inspect contents of Lua table in Neovim.
When using Lua inside of Neovim you may need to view the contents of Lua tables, which are a first class data structure in Lua world. Tables in Lua can represent ordinary arrays, lists, symbol tables, sets, records, graphs, trees, etc.
If you try to just print a table directly, you will get the reference address for that table instead of the content, which is not very useful for most debugging purposes:
:lua print(vim.api.nvim_get_mode()) " table: 0x7f5b93e5ff88
To solve this, Neovim provides the
vim.inspect
function as part of its API. It serializes the content of any Lua object into a human readable string.For example you can get information about the current mode like so:
:lua print(vim.inspect(vim.api.nvim_get_mode())) " { blocking = false, mode = "n"}
-
The
journald
logging driver sends container logs to the systemd journal. Log entries can be retrieved using thejournalctl
command, through use of the journal API, or using the docker logs command.In addition to the text of the log message itself, the
journald
log driver stores the following metadata in the journal with each message: | Field | Description | | --- | ---- | | CONTAINER_ID | The container ID truncated to 12 characters. | | CONTAINER_ID_FULL | The full 64-character container ID. | | CONTAINER_NAME | The container name at the time it was started. If you use docker rename to rename a container, the new name isn't reflected in the journal entries. | | CONTAINER_TAG, | SYSLOG_IDENTIFIER The container tag ( log tag option documentation). | | CONTAINER_PARTIAL_MESSAGE | A field that flags log integrity. Improve logging of long log lines. |To use the journald driver as the default logging driver, set the log-driver and log-opts keys to appropriate values in the
daemon.json
file, which is located in/etc/docker/
.{ "log-driver": "journald" }
Restart Docker for the changes to take effect.
-
There are many ways to send logs to loki
- Using the json driver and sending them to loki with promtail with the docker driver
- Using the docker plugin: Grafana Loki officially supports a Docker plugin that will read logs from Docker containers and ship them to Loki.
I would not recommend to use this path because there is a known issue that deadlocks the docker daemon :S. The driver keeps all logs in memory and will drop log entries if Loki is not reachable and if the quantity of
max_retries
has been exceeded. To avoid the dropping of log entries, settingmax_retries
to zero allows unlimited retries; the driver will continue trying forever until Loki is again reachable. Trying forever may have undesired consequences, because the Docker daemon will wait for the Loki driver to process all logs of a container, until the container is removed. Thus, the Docker daemon might wait forever if the container is stuck.The wait time can be lowered by setting
loki-retries=2
,loki-max-backoff_800ms
,loki-timeout=1s
andkeep-file=true
. This way the daemon will be locked only for a short time and the logs will be persisted locally when the Loki client is unable to re-connect.To avoid this issue, use the Promtail Docker service discovery. - Using the journald driver and sending them to loki with promtail with the journald driver. This has worked for me but the labels extracted are not that great.
-
New: Solve syslog getting filled up with docker network recreation.
If you find yourself with your syslog getting filled up by lines similar to:
Jan 15 13:19:19 home kernel: [174716.097109] eth2: renamed from veth0adb07e Jan 15 13:19:20 home kernel: [174716.145281] IPv6: ADDRCONF(NETDEV_CHANGE): vethcd477bc: link becomes ready Jan 15 13:19:20 home kernel: [174716.145337] br-1ccd0f48be7c: port 5(vethcd477bc) entered blocking state Jan 15 13:19:20 home kernel: [174716.145338] br-1ccd0f48be7c: port 5(vethcd477bc) entered forwarding state Jan 15 13:19:20 home kernel: [174717.081132] br-fbe765bc7d0a: port 2(veth31cdd6f) entered disabled state Jan 15 13:19:20 home kernel: [174717.081176] vethc4da041: renamed from eth0 Jan 15 13:19:21 home kernel: [174717.214911] br-fbe765bc7d0a: port 2(veth31cdd6f) entered disabled state Jan 15 13:19:21 home kernel: [174717.215917] device veth31cdd6f left promiscuous mode Jan 15 13:19:21 home kernel: [174717.215919] br-fbe765bc7d0a: port 2(veth31cdd6f) entered disabled state
It probably means that some docker is getting recreated continuously. Those traces are normal logs of docker creating the networks, but as they do each time the docker starts, if it's restarting continuously then you have a problem.
-
New: Minify the images.
dive and slim are two cli tools you can use to optimise the size of your dockers.
Logging⚑
-
New: Configure the logging module to log directly to systemd's journal.
To use
systemd.journal
in Python, you need to install thesystemd-python
package. This package provides bindings for systemd functionality.Install it using pip:
Below is an example Python script that configures logging to send messages to the systemd journal:pip install systemd-python
import logging from systemd.journal import JournalHandler logger = logging.getLogger('my_app') logger.setLevel(logging.DEBUG) # Set the logging level journal_handler = JournalHandler() journal_handler.setLevel(logging.DEBUG) # Adjust logging level if needed journal_handler.addFilter( lambda record: setattr(record, "SYSLOG_IDENTIFIER", "mbsync_syncer") or True ) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') journal_handler.setFormatter(formatter) logger.addHandler(journal_handler) logger.info("This is an info message.") logger.error("This is an error message.") logger.debug("Debugging information.")
When you run the script, the log messages will be sent to the systemd journal. You can view them using the
journalctl
command:sudo journalctl -f
This command will show the latest log entries in real time. You can filter by your application name using:
sudo journalctl -f -t my_app
Replace
my_app
with the logger name you used (e.g.,'my_app'
).Additional Tips - Tagging: You can add a custom identifier for your logs by setting
logging.getLogger('your_tag')
. This will allow you to filter logs usingjournalctl -t your_tag
. - Log Levels: You can control the verbosity of the logs by setting different levels (e.g.,DEBUG
,INFO
,WARNING
,ERROR
,CRITICAL
).Example Output in the Systemd Journal
You should see entries similar to the following in the systemd journal:
Nov 15 12:45:30 my_hostname my_app[12345]: 2024-11-15 12:45:30,123 - my_app - INFO - This is an info message. Nov 15 12:45:30 my_hostname my_app[12345]: 2024-11-15 12:45:30,124 - my_app - ERROR - This is an error message. Nov 15 12:45:30 my_hostname my_app[12345]: 2024-11-15 12:45:30,125 - my_app - DEBUG - Debugging information.
This approach ensures that your logs are accessible through standard systemd tools and are consistent with other system logs. Let me know if you have any additional requirements or questions!
SQLite⚑
-
import boto3 ec2 = boto3.client('ec2') running_instances = [ instance for page in ec2.get_paginator('describe_instances').paginate() for reservation in page['Reservations'] for instance in reservation['Instances']] if instance['State']['Name'] == 'running' ]
-
New: Order by a column descending.
SELECT select_list FROM table ORDER BY column_1 ASC, column_2 DESC;
Protocols⚑
-
New: Introduce Python Protocols.
The Python type system supports two ways of deciding whether two objects are compatible as types: nominal subtyping and structural subtyping.
Nominal subtyping is strictly based on the class hierarchy. If class Dog inherits class
Animal
, it’s a subtype ofAnimal
. Instances ofDog
can be used whenAnimal
instances are expected. This form of subtyping subtyping is what Python’s type system predominantly uses: it’s easy to understand and produces clear and concise error messages, and matches how the nativeisinstance
check works – based on class hierarchy.Structural subtyping is based on the operations that can be performed with an object. Class
Dog
is a structural subtype of classAnimal
if the former has all attributes and methods of the latter, and with compatible types.Structural subtyping can be seen as a static equivalent of duck typing, which is well known to Python programmers. See PEP 544 for the detailed specification of protocols and structural subtyping in Python.
Usage
You can define your own protocol class by inheriting the special Protocol class:
from typing import Iterable from typing_extensions import Protocol class SupportsClose(Protocol): # Empty method body (explicit '...') def close(self) -> None: ... class Resource: # No SupportsClose base class! def close(self) -> None: self.resource.release() # ... other methods ... def close_all(items: Iterable[SupportsClose]) -> None: for item in items: item.close() close_all([Resource(), open('some/file')]) # OK
Resource
is a subtype of theSupportsClose
protocol since it defines a compatible close method. Regular file objects returned byopen()
are similarly compatible with the protocol, as they supportclose()
.If you want to define a docstring on the method use the next syntax:
def load(self, filename: Optional[str] = None) -> None: """Load a configuration file.""" ...
Make protocols work with
isinstance
To check an instance against the protocol usingisinstance
, we need to decorate our protocol with@runtime_checkable
Make a protocol property variable
References - Mypy article on protocols - Predefined protocols reference
Logql⚑
-
New: Compare the values of a metric with the past.
The offset modifier allows changing the time offset for individual range vectors in a query.
For example, the following expression counts all the logs within the last ten minutes to five minutes rather than last five minutes for the MySQL job. Note that the offset modifier always needs to follow the range vector selector immediately.
count_over_time({job="mysql"}[5m] offset 5m) // GOOD count_over_time({job="mysql"}[5m]) offset 5m // INVALID
FastAPI⚑
-
New: Launch the server from within python.
import uvicorn if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
-
New: Add the request time to the logs.
For more information on changing the logging read 1
To set the datetime of the requests use this configuration
@asynccontextmanager async def lifespan(api: FastAPI): logger = logging.getLogger("uvicorn.access") console_formatter = uvicorn.logging.ColourizedFormatter( "{asctime} {levelprefix} : {message}", style="{", use_colors=True ) logger.handlers[0].setFormatter(console_formatter) yield api = FastAPI(lifespan=lifespan)
Graphql⚑
-
New: Introduce GraphQL.
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
Pytest⚑
nodejs⚑
-
New: Install using nvm.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash nvm install 22 node -v # should print `v22.12.0` npm -v # should print `10.9.0`
Python Snippets⚑
-
New: Get unique items between two lists.
If you want all items from the second list that do not appear in the first list you can write:
x = [1,2,3,4] f = [1,11,22,33,44,3,4] result = set(f) - set(x)
-
number = 1 print(f"{number:02d}")
-
New: Parse a datetime from an epoch.
>>> import datetime >>> datetime.datetime.fromtimestamp(1347517370).strftime('%c') '2012-09-13 02:22:50'
-
New: Fix variable is unbound pyright error.
You may receive these warnings if you set variables inside if or try/except blocks such as the next one:
def x(): y = True if y: a = 1 print(a) # "a" is possibly unbound
The easy fix is to set
a = None
outside those blocksdef x(): a = None y = True if y: a = 1 print(a) # "a" is possibly unbound
-
New: Expire the cache of the lru_cache.
The
lru_cache
decorator caches forever, a way to prevent it is by adding one more parameter to your expensive function:ttl_hash=None
. This new parameter is so-called "time sensitive hash", its the only purpose is to affect lru_cache. For example:from functools import lru_cache import time @lru_cache() def my_expensive_function(a, b, ttl_hash=None): del ttl_hash # to emphasize we don't use it and to shut pylint up return a + b # horrible CPU load... def get_ttl_hash(seconds=3600): """Return the same value withing `seconds` time period""" return round(time.time() / seconds) res = my_expensive_function(2, 2, ttl_hash=get_ttl_hash())
-
New: Kill a process by it's PID.
import os import signal os.kill(pid, signal.SIGTERM) #or signal.SIGKILL
-
New: Convert the parameter of an API get request to a valid field.
For example if the argument has
/
:from urllib.parse import quote quote("value/with/slashes")
Will return
value%2Fwith%2Fslashes
-
New: Get the type hints of an object.
```python from typing import get_type_hints
Student(NamedTuple): name: Annotated[str, 'some marker']
get_type_hints(Student) == {'name': str} get_type_hints(Student, include_extras=False) == {'name': str} get_type_hints(Student, include_extras=True) == { 'name': Annotated[str, 'some marker'] } ````
-
New: Type hints of a python module.
from types import ModuleType import os assert isinstance(os, ModuleType)
-
New: Get all the classes of a python module.
def _load_classes_from_directory(self, directory): classes = [] for file_name in os.listdir(directory): if file_name.endswith(".py") and file_name != "__init__.py": module_name = os.path.splitext(file_name)[0] module_path = os.path.join(directory, file_name) # Import the module dynamically spec = spec_from_file_location(module_name, module_path) if spec is None or spec.loader is None: raise ValueError( f"Error loading the spec of {module_name} from {module_path}" ) module = module_from_spec(spec) spec.loader.exec_module(module) # Retrieve all classes from the module module_classes = inspect.getmembers(module, inspect.isclass) classes.extend(module_classes)
-
New: Import files from other directories.
Add the directory where you have your function to
sys.path
import sys sys.path.append("**Put here the directory where you have the file with your function**") from file import function
-
New: Use Path of pathlib write_text in append mode.
It's not supported you need to
open
it:with my_path.open("a") as f: f.write("...")
-
New: Suppress ANN401 for dynamically typed *args and **kwargs.
Use
object
instead:def function(*args: object, **kwargs: object) -> None:
-
To write an if-then-else statement in Python so that it fits on one line you can use:
fruit = 'Apple' isApple = True if fruit == 'Apple' else False
-
New: Get package data relative path.
If you want to reference files from the foo/package1/resources folder you would want to use the file variable of the module. Inside foo/package1/init.py:
from os import path resources_dir = path.join(path.dirname(__file__), 'resources')
-
New: Compare file and directories.
The filecmp module defines functions to compare files and directories, with various optional time/correctness trade-offs. For comparing files, see also the difflib module.
from filecmp import dircmp def print_diff_files(dcmp): for name in dcmp.diff_files: print("diff_file %s found in %s and %s" % (name, dcmp.left, dcmp.right)) for sub_dcmp in dcmp.subdirs.values(): print_diff_files(sub_dcmp) dcmp = dircmp('dir1', 'dir2') print_diff_files(dcmp)
-
New: Send a linux desktop notification.
To show a Linux desktop notification from a Python script, you can use the
notify2
library (although it's last commit was done on 2017. This library provides an easy way to send desktop notifications on Linux.Alternatively, you can use the
subprocess
module to call thenotify-send
command-line utility directly. This is a more straightforward method but requiresnotify-send
to be installed.import subprocess def send_notification(title: str, message: str = "", urgency: str = "normal") -> None: """Send a desktop notification using notify-send. Args: title (str): The title of the notification. message (str): The message body of the notification. Defaults to an empty string. urgency (str): The urgency level of the notification. Can be 'low', 'normal', or 'critical'. Defaults to 'normal'. """ subprocess.run(["notify-send", "-u", urgency, title, message])
-
import traceback def cause_error(): return 1 / 0 # This will raise a ZeroDivisionError try: cause_error() except Exception as error: # Capture the exception traceback as a string error_message = "".join(traceback.format_exception(None, error, error.__traceback__)) print("An error occurred:\n", error_message)
Goodconf⚑
-
New: Initialize the config with a default value if the file doesn't exist.
feat(goodconf#Config saving)def load(self, filename: Optional[str] = None) -> None: self._config_file = filename if not self.store_dir.is_dir(): log.warning("The store directory doesn't exist. Creating it") os.makedirs(str(self.store_dir)) if not Path(self.config_file).is_file(): log.warning("The yaml store file doesn't exist. Creating it") self.save() super().load(filename)
So far
goodconf
doesn't support saving the config. Until it's ready you can use the next snippet:feat(google_chrome#Open a specific profile): Open a specific profileclass YamlStorage(GoodConf): """Adapter to store and load information from a yaml file.""" @property def config_file(self) -> str: """Return the path to the config file.""" return str(self._config_file) @property def store_dir(self) -> Path: """Return the path to the store directory.""" return Path(self.config_file).parent def reload(self) -> None: """Reload the contents of the authentication store.""" self.load(self.config_file) def load(self, filename: Optional[str] = None) -> None: """Load a configuration file.""" if not filename: filename = f"{self.store_dir}/data.yaml" super().load(self.config_file) def save(self) -> None: """Save the contents of the authentication store.""" with open(self.config_file, "w+", encoding="utf-8") as file_cursor: yaml = YAML() yaml.default_flow_style = False yaml.dump(self.dict(), file_cursor)
google-chrome --profile-directory="Profile Name"
Where
Profile Name
is one of the profiles listed underls ~/.config/chromium | grep -i profile
.
Inotify⚑
-
New: Introduce python_inotify.
inotify is a python library that acts as a bridge to the
inotify
linux kernel which allows you to register one or more directories for watching, and to simply block and wait for notification events. This is obviously far more efficient than polling one or more directories to determine if anything has changed.Installation:
pip install inotify
Basic example using a loop:
import inotify.adapters def _main(): i = inotify.adapters.Inotify() i.add_watch('/tmp') with open('/tmp/test_file', 'w'): pass for event in i.event_gen(yield_nones=False): (_, type_names, path, filename) = event print("PATH=[{}] FILENAME=[{}] EVENT_TYPES={}".format( path, filename, type_names)) if __name__ == '__main__': _main()
Output:
PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_MODIFY'] PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_OPEN'] PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_CLOSE_WRITE']
Basic example without a loop:
import inotify.adapters def _main(): i = inotify.adapters.Inotify() i.add_watch('/tmp') with open('/tmp/test_file', 'w'): pass events = i.event_gen(yield_nones=False, timeout_s=1) events = list(events) print(events) if __name__ == '__main__': _main()
The wait will be done in the
list(events)
line -
Correction: Deprecate inotify.
DEPRECATED: As of 2024-11-15 it's been 4 years since the last commit. watchdog has 6.6k stars and last commit was done 2 days ago.
watchdog⚑
-
New: Introduce watchdog.
watchdog is a Python library and shell utilities to monitor filesystem events.
Cons:
- The docs suck.
Installation
pip install watchdog
Usage
A simple program that uses watchdog to monitor directories specified as command-line arguments and logs events generated:
import time from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer class MyEventHandler(FileSystemEventHandler): def on_any_event(self, event: FileSystemEvent) -> None: print(event) event_handler = MyEventHandler() observer = Observer() observer.schedule(event_handler, ".", recursive=True) observer.start() try: while True: time.sleep(1) finally: observer.stop() observer.join()
Pydantic⚑
-
New: Nicely show validation errors.
A nice way of showing it is to capture the error and print it yourself:
try: model = Model( state=state, ) except ValidationError as error: log.error(f'Error building model with state {state}') raise error
-
New: Load a pydantic model from json.
You can use the
model_validate_json
method that will validate and return an object with the loaded data.from datetime import date from pydantic import BaseModel, ConfigDict, ValidationError class Event(BaseModel): model_config = ConfigDict(strict=True) when: date where: tuple[int, int] json_data = '{"when": "1987-01-28", "where": [51, -1]}' print(Event.model_validate_json(json_data)) try: Event.model_validate({'when': '1987-01-28', 'where': [51, -1]}) except ValidationError as e: print(e) """ 2 validation errors for Event when Input should be a valid date [type=date_type, input_value='1987-01-28', input_type=str] where Input should be a valid tuple [type=tuple_type, input_value=[51, -1], input_type=list] """
-
New: Create part of the attributes in the initialization stage.
class Sqlite(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) path: Path db: sqlite3.Cursor def __init__(self, **kwargs): conn = sqlite3.connect(kwargs['path']) kwargs['db'] = conn.cursor() super().__init__(**kwargs)
psycopg2⚑
-
New: Introduce psycopg2.
Installation
Install the dependencies:
sudo apt install libpq-dev python3-dev
Then install the package
pip install psycopg2
questionary⚑
-
New: Unit testing questionary code.
Testing
questionary
code can be challenging because it involves interactive prompts that expect user input. However, there are ways to automate the testing process. You can use libraries likepexpect
,pytest
, andpytest-mock
to simulate user input and test the behavior of your code.Here’s how you can approach testing
questionary
code usingpytest-mock
to mockquestionary
functionsYou can mock
questionary
functions likequestionary.select().ask()
to simulate user choices without actual user interaction.Testing a single
questionary.text
promptLet's assume you have a function that asks the user for their name:
import questionary def ask_name() -> str: name = questionary.text("What's your name?").ask() return name
You can test this function by mocking the
questionary.text
prompt to simulate the user's input.import pytest from your_module import ask_name def test_ask_name(mocker): # Mock the text function to simulate user input mock_text = mocker.patch('questionary.text') # Define the response for the prompt mock_text.return_value.ask.return_value = "Alice" result = ask_name() assert result == "Alice"
Test a function that has many questions
Here’s an example of how to test a function that contains two
questionary.text
prompts usingpytest-mock
.Let's assume you have a function that asks for the first and last names of a user:
import questionary def ask_full_name() -> dict: first_name = questionary.text("What's your first name?").ask() last_name = questionary.text("What's your last name?").ask() return {"first_name": first_name, "last_name": last_name}
You can mock both
questionary.text
calls to simulate user input for both the first and last names:import pytest from your_module import ask_full_name def test_ask_full_name(mocker): # Mock the text function for the first name prompt mock_text_first = mocker.patch('questionary.text') # Define the response for the first name prompt mock_text_first.side_effect = ["Alice", "Smith"] result = ask_full_name() assert result == {"first_name": "Alice", "last_name": "Smith"}
rich⚑
-
New: Adding a footer to a table.
Adding a footer is not easy task. This answer doesn't work anymore as
table
doesn't have theadd_footer
. You need to create the footer in theadd_column
so you need to have the data that needs to go to the footer before building the rows.You would do something like:
table = Table(title="Star Wars Movies", show_footer=True) table.add_column("Title", style="magenta", footer='2342')
Coding tools⚑
Singer⚑
-
New: Introduce neotree.
General keymaps:
<cr>
: Open the file in the current buffer<s>
: Open in a vertical split<S>
: Open in an horizontal split<bs>
: Navigate one directory up (even if it's the root of thecwd
)
File and directory management:
a
: Create a new file or directory. Add a/
to the end of the name to make a directory.d
: Delete the selected file or directoryr
: Rename the selected file or directoryy
: Mark file to be copied (supports visual selection)x
: Mark file to be cut (supports visual selection)m
: Move the selected file or directoryc
: Copy the selected file or directory
References:
-
New: Show hidden files.
return { "nvim-neo-tree/neo-tree.nvim", opts = { filesystem = { filtered_items = { visible = true, show_hidden_count = true, hide_dotfiles = false, hide_gitignored = true, hide_by_name = { '.git', }, never_show = {}, }, } } }
-
This example uses the file_open event to close the Neo-tree window when a file is opened. This applies to all windows and all sources at once.
require("neo-tree").setup({ event_handlers = { { event = "file_opened", handler = function(file_path) -- auto close -- vimc.cmd("Neotree close") -- OR require("neo-tree.command").execute({ action = "close" }) end }, } })
-
Copy the code under implementation in your config file.
-
New: Can't copy file/directory to itself.
If you want to copy a directory you need to assume that the prompt is done from within the directory. So if you want to copy it to a new name at the same level you need to use
../new-name
instead ofnew-name
. -
New: Introduce the vim foldings workflow.
ne way to easily work with folds is by using the fold-cycle plugin to be able to press
<tab>
or<enter>
to toggle a fold.If you're using lazyvim you can use the next configuration:
return { { "jghauser/fold-cycle.nvim", config = function() require("fold-cycle").setup() end, keys = { { "<tab>", function() return require("fold-cycle").open() end, desc = "Fold-cycle: open folds", silent = true, }, { "<cr>", function() return require("fold-cycle").open() end, desc = "Fold-cycle: open folds", silent = true, }, { "<s-tab>", function() return require("fold-cycle").close() end, desc = "Fold-cycle: close folds", silent = true, }, { "zC", function() return require("fold-cycle").close_all() end, remap = true, silent = true, desc = "Fold-cycle: close all folds", }, }, }, }
-
New: Introduce singer.
Singer is an open-source standard for writing scripts that move data.
It describes how data extraction scripts—called “taps” —and data loading scripts—called “targets”— should communicate, allowing them to be used in any combination to move data from any source to any destination. Send data between databases, web APIs, files, queues, and just about anything else you can think of.
It has many "taps" and "targets" that can help you interact with third party tools without needing to write the code.
References - Home
-
New:
is not well mapped. It's because
<c-i>
is a synonym of<TAB>
.
Coding with AI⚑
-
New: Introduce LazyVim.
-
New: Adding plugins configuration.
Configuring LazyVim plugins is exactly the same as using
lazy.nvim
to build a config from scratch.For the full plugin spec documentation please check the lazy.nvim readme.
LazyVim comes with a list of preconfigured plugins, check them here before diving on your own.
-
New: Adding a plugin.
Adding a plugin is as simple as adding the plugin spec to one of the files under `lua/plugins/*.lua``. You can create as many files there as you want.
You can structure your
lua/plugins`` folder with a file per plugin, or a separate file containing all the plugin specs for some functionality. For example:
lua/plugins/lsp.lua`return { -- add symbols-outline { "simrat39/symbols-outline.nvim", cmd = "SymbolsOutline", keys = { { "<leader>cs", "<cmd>SymbolsOutline<cr>", desc = "Symbols Outline" } }, opts = { -- add your options that should be passed to the setup() function here position = "right", }, }, }
Customizing plugin specs. Defaults merging rules:
- cmd: the list of commands will be extended with your custom commands
- event: the list of events will be extended with your custom events
- ft: the list of filetypes will be extended with your custom filetypes
- keys: the list of keymaps will be extended with your custom keymaps
- opts: your custom opts will be merged with the default opts
- dependencies: the list of dependencies will be extended with your custom dependencies
- any other property will override the defaults
For ft, event, keys, cmd and opts you can instead also specify a values function that can make changes to the default values, or return new values to be used instead.
-- change trouble config { "folke/trouble.nvim", -- opts will be merged with the parent spec opts = { use_diagnostic_signs = true }, } -- add cmp-emoji { "hrsh7th/nvim-cmp", dependencies = { "hrsh7th/cmp-emoji" }, ---@param opts cmp.ConfigSchema opts = function(_, opts) table.insert(opts.sources, { name = "emoji" }) end, }
Defining the plugin keymaps:
Adding
keys=
follows the rules as explained above. You don't have to specify a mode fornormal
mode keymaps.You can also disable a default keymap by setting it to
false
. To override a keymap, simply add one with the samelhs
and a newrhs
. For examplelua/plugins/telescope.lua
return { "nvim-telescope/telescope.nvim", keys = { -- disable the keymap to grep files {"<leader>/", false}, -- change a keymap { "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find Files" }, -- add a keymap to browse plugin files { "<leader>fp", function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end, desc = "Find Plugin File", }, }, },
Make sure to use the exact same mode as the keymap you want to disable.
You can also return a whole new set of keymaps to be used instead. Or returnreturn { "folke/flash.nvim", keys = { -- disable the default flash keymap { "s", mode = { "n", "x", "o" }, false }, }, }
{}
to disable all keymaps for a plugin.return { "nvim-telescope/telescope.nvim", -- replace all Telescope keymaps with only one mapping keys = function() return { { "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find Files" }, } end, },
-
New: Auto update plugins.
Add this to
~/.config/nvim/lua/config/autocomds.lua
local function augroup(name) return vim.api.nvim_create_augroup("lazyvim_" .. name, { clear = true }) end vim.api.nvim_create_autocmd("VimEnter", { group = augroup("autoupdate"), callback = function() if require("lazy.status").has_updates then require("lazy").update({ show = false }) end end, })
-
New: Introduce vim keymaps.
LazyVim comes with some sane default keybindings, you can see them here. You don't need to remember them all, it also comes with which-key to help you remember your keymaps. Just press any key like
and you'll see a popup with all possible keymaps starting with . - default
<leader>
is<space>
- default
<localleader>
is\
General editor bindings:
- Save file:
<C-s>
- Quit all:
<leader>qq
- Open a floating terminal:
<C-/>
Movement keybindings:
- Split the windows:
- Vertically:
<C-|
- Horizontally:
<C--
- Delete window:
<leader>wd
- To move around the windows you can use:
, , , . - To resize the windows use:
, , , - To move between buffers:
- Next and previous with
, - Switch to the previously opened buffer:
<leader>bb
Coding keybindings:
Diagnostics:
<leader>cd>
: Shows you the diagnostics message of the current line in a floating window]d
and[d
: iterates over all diagnostics]e
and[e
: iterates over all error diagnostics]w
and[w
: iterates over all warning diagnostics
- default
-
If you need to set keymaps in lua you can use
vim.keymap.set
. For example:vim.keymap.set('n', '<space>w', '<cmd>write<cr>', {desc = 'Save'})
After executing this, the sequence
Space + w
will call thewrite
command. Basically, we can save changes made to a file withSpace + w
.Let's dive into what does the
vim.keymap.set
parameters mean.vim.keymap.set({mode}, {lhs}, {rhs}, {opts})
{mode}
: mode where the keybinding should execute. It can be a list of modes. We need to speify the mode's short name. Here are some of the most common.n
: Normal mode.i
: Insert mode.x
: Visual mode.s
: Selection mode.v
: Visual + Selection.t
: Terminal mode.o
: Operator-pending.-
''
: Yes, an empty string. Is the equivalent ofn + v + o
. -
{lhs}
: is the key we want to bind. {rhs}
is the action we want to execute. It can be a string with a command or an expression. You can also provide a lua function.-
{opts}
this must be a lua table. If you don't know what is a "lua table" just think is a way of storing several values in one place. Anyway, it can have these properties. -
desc
: A string that describes what the keybinding does. You can write anything you want. remap
: A boolean that determines if our keybinding can be recursive. The default value isfalse
. Recursive keybindings can cause some conflicts if used incorrectly. Don't enable it unless you know what you're doing.buffer
: It can be a boolean or a number. If we assign the booleantrue
it means the keybinding will only be effective in the current file. If we assign a number, it needs to be the "id" of an open buffer.silent
: A boolean. Determines whether or not the keybindings can show a message. The default value isfalse
.expr
: A boolean. If enabled it gives the chance to use vimscript or lua to calculate the value of{rhs}
. The default value isfalse
.
-
New: The leader key.
When creating keybindings we can use the special sequence
<leader>
in the{lhs}
parameter, it'll take the value of the global variablemapleader
.So
mapleader
is a global variable in vimscript that can be string. For example.vim.g.mapleader = ' '
After defining it we can use it as a prefix in our keybindings.
vim.keymap.set('n', '<leader>w', '<cmd>write<cr>')
This will make
<space key>
+w
save the current file.There are different opinions on what key to use as the
<leader>
key. The<space>
is the most comfortable as it's always close to your thumbs, and it works well with both hands. Nevertheless, you can only use it in normal mode, because in insert<space><whatever>
will be triggered as you write. An alternative is to use;
which is also comfortable (if you use the english key distribution) and you can use it in insert mode.If you want to define more than one leader key you can either:
- Change the
mapleader
many times in your file: As the value ofmapleader
is used at the moment the mapping is defined, you can indeed change that while plugins are loading. For that, you have to explicitly:runtime
the plugins in your~/.vimrc
(and count on the canonical include guard to prevent redefinition later):
* Use the keys directly instead of usinglet mapleader = ',' runtime! plugin/NERD_commenter.vim runtime! ... let mapleader = '\' runime! plugin/mark.vim ...
<leader>
" editing mappings nnoremap ,a <something> nnoremap ,k <something else> nnoremap ,d <and something else> " window management mappings nnoremap gw <something> nnoremap gb <something else>
Defining
mapleader
and/or using<leader>
may be useful if you change your mind often on what key to use a leader but it won't be of any use if your mappings are stable. - Change the
-
New: Configure vim from scratch.
Neovim configuration is a complex thing to do, both to start and to maintain. The configurations are endless, the plugins are too. Be ready to spend a lot of energy on it and to get lost reading a lot.
If I'm scaring you, you are right to be scared! xD. Once you manage to get it configured to your liking you'll think that in the end it doesn't even matter spending all that time. However if you're searching for something that is plug and play try vscodium.
To make things worse, the configuration is done in lua, so you may need a small refreshment to understand what are you doing.
-
New: Vim distributions.
One way to make vim's configuration more bearable is to use vim distributions. These are projects that maintain configurations with sane defaults and that work with the whole ecosystem of plugins.
Using them is the best way to:
- Have something usable fast
- Minimize the maintenance efforts as others are doing it for you (plugin changes, breaking changes, ..)
- Keep updated with the neovim ecosystem, as you can see what is the community adding to the default config.
However, there are so many good Neovim configuration distributions that it becomes difficult for a Neovim user to decide which distribution to use and how to tailor it for their use case.
By far, the top 5 Neovim configuration distributions are AstroNvim, kickstart, LazyVim, LunarVim, and NvChad. That is not to say these are the “best” configuration distributions, simply that they are the most popular.
Each of these configuration distributions has value. They all provide excellent starting points for crafting your own custom configuration, they are all extensible and fairly easy to learn, and they all provide an out-of-the-box setup that can be used effectively without modification.
Distinguishing features of the top Neovim configuration distributions are:
-
AstroNvim:
- An excellent community repository
- Fully featured out-of-the-box
- Good documentation
-
kickstart
- Minimal out-of-the-box setup
- Easy to extend and widely used as a starting point
- A good choice if your goal is hand-crafting your own config
-
LazyVim
- Very well maintained by the author of lazy.nvim
- Nice architecture, it’s a plugin with which you can import preconfigured plugins
- Good documentation
-
LunarVim
- Well maintained and mature
- Custom installation processs installs LunarVim in an isolated location
- Been around a while, large community, widespread presence on the web
-
NvChad
- Really great base46 plugin enables easy theme/colorscheme management
- Includes an impressive mappings cheatsheet
- ui plugin and nvim-colorizer
Personally I tried LunarVim and finally ended up with LazyVim because:
- It's more popular
- I like the file structure
- It's being maintained by folke one of the best developers of neovim plugins.
-
New: Starting your configuration with LazyVim.
LazyVim needs the next tools to be able to work:
- Neovim >= 0.9.0 (needs to be built with LuaJIT). Follow these instructions
- Git >= 2.19.0 (for partial clones support).
sudo apt-get install git
. - a Nerd Font(v3.0 or greater) (optional, but strongly suggested as they rae needed to display some icons). Follow these instructions if you're using kitty.
- lazygit (optional and I didn't like it)
- a C compiler for nvim-treesitter.
apt-get install gcc
- for telescope.nvim (optional)
- live grep:
ripgrep
- find files:
fd
- a terminal that support true color and undercurl:
- kitty (Linux & Macos)
- wezterm (Linux, Macos & Windows)
- alacritty (Linux, Macos & Windows)
- iterm2 (Macos)
- Make a backup of your current Neovim files:
# required mv ~/.config/nvim{,.old} # optional but recommended mv ~/.local/share/nvim{,.old} mv ~/.local/state/nvim{,.old} mv ~/.cache/nvim{,.old}
-
Clone the starter
git clone https://github.com/LazyVim/starter ~/.config/nvim
-
Remove the
.git
folder, so you can add it to your own repo laterrm -rf ~/.config/nvim/.git
-
Start Neovim!
- It is recommended to runnvim
:LazyHealth
after installation. This will load all plugins and check if everything is working correctly.
Understanding the file structure:
The files under
config
will be automatically loaded at the appropriate time, so you don't need to require those files manually.You can add your custom plugin specs under
lua/plugins/
. All files there will be automatically loaded by lazy.nvim.The files~/.config/nvim ├── lua │ ├── config │ │ ├── autocmds.lua │ │ ├── keymaps.lua │ │ ├── lazy.lua │ │ └── options.lua │ └── plugins │ ├── spec1.lua │ ├── ** │ └── spec2.lua └── init.toml
autocmds.lua
,keymaps.lua
,lazy.lua
andoptions.lua
underlua/config
will be automatically loaded at the appropriate time, so you don't need to require those files manually. LazyVim comes with a set of default config files that will be loaded before your own.You can continue your config by adding plugins.
-
New: Introduce the vim movement workflow.
Moving around vim can be done in many ways, which an lead to being lost on how to do it well.
LazyVim has a very nice way to deal with buffers - Use
H
andL
if the buffer you want to go to is visually close to where you are. - Otherwise, if the buffer is open, use<leader>,
- For other files, use<leader><space>
- Close buffers you no longer need with<leader>bd
-<leader>ss
to quickly jump to a function in the buffer you're on - Using the jump list with<c-o>
,<c-i>
andgd
to navigate the code - You can pin buffers with<leader>bp
and delete all non pinned buffers with<leader>bP
-
New: Using the jump list.
Vim has a feature called the “Jump List”, which saves all the locations you’ve recently visited, including their line number, column number, and what else not in the
.viminfo
file, to help you get exactly the position you were last in. Not only does it save the locations in your current buffer, but also previous buffers you may have edited in other Vim sessions. Which means, if you’re currently working on a file, and there aren’t many last-location saves in this one, you’ll be redirected to the previous file you had edited. But how do you do that? Simply pressCtrl + O
, and it’ll get you back to the previous location you were in, or more specifically, your cursor was in.If you want to go back to the newer positions, after you’re done with what you wanted to do, you can then press
Ctrl + i
to go back to the newer position. This is exceptionally useful when you’re working with a lot of project files at a time, and you need to go back and forth between multiple blocks in different files. This could instantly give you a boost, as you won’t need to have separate buffers opened up or windows to be setted up, you can simply jump between the files and edit them.Ctrl + O is probably not meant for a single task, as far as Vim’s philosophy is concerned. The jumping mentioned in the previous section only works when you’re in Normal Mode, and not when you’re in Insert Mode. When you press Ctrl + O in Insert Mode, what happens instead is that you’ll enter Normal Mode, and be able to execute a single command, after which Vim will automatically switch back to Insert Mode.
-
return { { "sindrets/diffview.nvim", dependencies = { { "nvim-tree/nvim-web-devicons", lazy = true }, }, keys = { { "dv", function() if next(require("diffview.lib").views) == nil then vim.cmd("DiffviewOpen") else vim.cmd("DiffviewClose") end end, desc = "Toggle Diffview window", }, }, }, }
Which sets the next bindings: -
dv
: Toggle the opening and closing of the diffview windows -
New: Use diffview as merge tool.
Add to your
~/.gitconfig
:[alias] mergetool = "!nvim -c DiffviewOpen"
-
If you call
:DiffviewOpen
during a merge or a rebase, the view will list the conflicted files in their own section. When opening a conflicted file, it will open in a 3-way diff allowing you to resolve the conflict with the context of the target branch's version (OURS, left), and the version from the branch which is being merged (THEIRS, right).The conflicted file's entry in the file panel will show the remaining number of conflict markers (the number following the file name). If what follows the file name is instead an exclamation mark (
!
), this indicates that the file has not yet been opened, and the number of conflicts is unknown. If the sign is a check mark, this indicates that there are no more conflicts in the file.You can interact with the merge tool with the next bindings:
]x
and[x
: Jump between conflict markers. This works from the file panel as well.dp
: Put the contents on the other bufferdo
: Get the contents from the other buffer2do
: to obtain the hunk from the OURS side of the diff3do
to obtain the hunk from the THEIRS side of the diff1do
to obtain the hunk from the BASE in a 4-way diff
Additionally there are mappings for operating directly on the conflict markers:
<leader>co
: Choose the OURS version of the conflict.<leader>ct
: Choose the THEIRS version of the conflict.<leader>cb
: Choose the BASE version of the conflict.<leader>ca
: Choose all versions of the conflict (effectively just deletes the markers, leaving all the content).dx
: Choose none of the versions of the conflict (delete the conflict region).
-
New: Introduce ai coding prompts.
These are some useful AI prompts to help you while you code:
- create a function with type hints and docstring using google style called { } that { }
- create the tests for the function { } adding type hints and following the AAA style where the Act section is represented contains a returns = (thing to test) line or if the function to test doesn't return any value append an # act comment at the end of the line. Use paragraphs to separate the AAA blocks and don't add comments inside the tests for the sections
If you use espanso you can simplify the filling up of these prompts on the AI chats. For example:
--- matches: - trigger: :function form: | Create a function with type hints and docstring using google style called [[name]] that: [[text]] form_fields: text: multiline: true - trigger: :tweak form: | Tweak the next code: [[code]] So that: [[text]] form_fields: text: multiline: true code: multiline: true - trigger: :test form: | create the tests for the function: [[text]] Following the next guidelines: - Add type hints - Follow the AAA style - In the Act section if the function to test returns a value always name that variable returns. If the function to test doesn't return any value append an # act comment at the end of the line. - Use paragraphs to separate the AAA blocks and don't add comments like # Arrange or # Act or # Act/Assert or # Assert form_fields: text: multiline: true - trigger: :refactor form: | Refactor the next code [[code]] with the next conditions [[conditions]] form_fields: code: multiline: true conditions: multiline: true
-
New: Introduce Kestra.
Kestra is an open-source orchestrator designed to bring Infrastructure as Code (IaC) best practices to all workflows — from those orchestrating mission-critical operations, business processes, and data pipelines to simple Zapier-style automation. Built with an API-first philosophy, Kestra enables users to define and manage data pipelines through a simple YAML configuration file. This approach frees you from being tied to a specific client implementation, allowing for greater flexibility and easier integration with various tools and services.
Look at this 4 minute video for a visual introduction
References - Docs - Home - 4 minute introduction video
-
New: Add new prompts for developers.
- trigger: :polish form: | Polish the next code [[code]] with the next conditions: - Use type hints on all functions and methods - Add or update the docstring using google style on all functions and methods form_fields: code: multiline: true - trigger: :commit form: | Act as an expert developer. Create a message commit with the next conditions: - follow semantic versioning - create a semantic version comment per change - include all comments in a raw code block so that it's easy to copy for the following diff [[text]] form_fields: text: multiline: true
-
Correction: Update the ai prompts.
```yaml matches: - trigger: :function form: | Create a function with: - type hints - docstrings for all classes, functions and methods - docstring using google style with line length less than 89 characters - adding logging traces using the log variable log = logging.getLogger(name) - Use fstrings instead of %s - If you need to open or write a file always set the encoding to utf8 - If possible add an example in the docstring - Just give the code, don't explain anything
Called [[name]] that: [[text]] form_fields: text: multiline: true
-
trigger: :class form: | Create a class with:
- type hints
- docstring using google style with line length less than 89 characters
- use docstrings on the class and each methods
- adding logging traces using the log variable log = logging.getLogger(name)
- Use fstrings instead of %s
- If you need to open or write a file always set the encoding to utf8
- If possible add an example in the docstring
- Just give the code, don't explain anything
Called [[name]] that: [[text]] form_fields: text: - trigger: :class form: | ... - Use paragraphs to separate the AAA blocks and don't add comments like # Arrange or # Act or # Act/Assert or # Assert. So the test will only have black lines between sections - In the Act section if the function to test returns a value always name that variable result. If the function to test doesn't return any value append an # act comment at the end of the line. - If the test uses a pytest.raises there is no need to add the # act comment - Don't use mocks - Use fstrings instead of %s - Gather all tests over the same function on a common class - If you need to open or write a file always set the encoding to utf8 - Just give the code, don't explain anything
form_fields: text: - trigger: :polish form: | ... - Add or update the docstring using google style on all classes, functions and methods - Wrap the docstring lines so they are smaller than 89 characters - All docstrings must start in the same line as the """ - Add logging traces using the log variable log = logging.getLogger(name) - Use f-strings instead of %s - Just give the code, don't explain anything form_fields: code: multiline: true - trigger: :text form: | Polish the next text by:
- Summarising each section without losing relevant data
- Tweak the markdown format
- Improve the wording
[[text]] form_fields: text: multiline: true
-
trigger: :readme form: | Create the README.md taking into account:
- Use GPLv3 for the license
- Add Lyz as the author
- Add an installation section
- Add an usage section
of: [[text]]
form_fields: text: multiline: true ``` feat(aleph#Get all documents of a collection): Get all documents of a collection
list_aleph_collection_documents.py
is a Python script designed to interact with an API to retrieve and analyze documents from specified collections. It offers a command-line interface (CLI) to list and check documents within a specified collection.Features
- Retrieve documents from a specified collection.
- Analyze document processing statuses and warn if any are not marked as successful.
- Return a list of filenames from the retrieved documents.
- Supports verbose output for detailed logging.
- Environment variable support for API key management.
Installation
To install the required dependencies, use
pip
:pip install typer requests
Ensure you have Python 3.6 or higher installed.
Create the file
list_aleph_collection_documents.py
with the next contents:import logging import requests from typing import List, Dict, Any, Optional import logging import typer from typing import List, Dict, Any log = logging.getLogger(__name__) app = typer.Typer() @app.command() def get_documents( collection_name: str = typer.Argument(...), api_key: Optional[str] = typer.Option(None, envvar="API_KEY"), base_url: str = typer.Option("https://your.aleph.org"), verbose: bool = typer.Option( False, "--verbose", "-v", help="Enable verbose output" ), ): """CLI command to retrieve documents from a specified collection.""" if verbose: logging.basicConfig(level=logging.DEBUG) log.debug("Verbose mode enabled.") else: logging.basicConfig(level=logging.INFO) if api_key is None: log.error( "Please specify your api key either through the --api-key argument " "or through the API_KEY environment variable" ) raise typer.Exit(code=1) try: documents = list_collection_documents(api_key, base_url, collection_name) filenames = check_documents(documents) if filenames: print("\n".join(filenames)) else: log.warning("No documents found.") except Exception as e: log.error(f"Failed to retrieve documents: {e}") raise typer.Exit(code=1) def list_collection_documents( api_key: str, base_url: str, collection_name: str ) -> List[Dict[str, Any]]: """ Retrieve documents from a specified collection using pagination. Args: api_key (str): The API key for authentication. base_url (str): The base URL of the API. collection_name (str): The name of the collection to retrieve documents from. Returns: List[Dict[str, Any]]: A list of documents from the specified collection. Example: >>> docs = list_collection_documents("your_api_key", "https://api.example.com", "my_collection") >>> print(len(docs)) 1000 """ headers = { "Authorization": f"ApiKey {api_key}", "Accept": "application/json", "Content-Type": "application/json", } collections_url = f"{base_url}/api/2/collections" documents_url = f"{base_url}/api/2/entities" log.debug(f"Requesting collections list from {collections_url}") collections = [] params = {"limit": 300} while True: response = requests.get(collections_url, headers=headers, params=params) response.raise_for_status() data = response.json() collections.extend(data["results"]) log.debug( f"Fetched {len(data['results'])} collections, " f"page {data['page']} of {data['pages']}" ) if not data["next"]: break params["offset"] = params.get("offset", 0) + data["limit"] collection_id = next( (c["id"] for c in collections if c["label"] == collection_name), None ) if not collection_id: log.error(f"Collection {collection_name} not found.") return [] log.info(f"Found collection '{collection_name}' with ID {collection_id}") documents = [] params = { "q": "", "filter:collection_id": collection_id, "filter:schemata": "Document", "limit": 300, } while True: log.debug(f"Requesting documents from collection {collection_id}") response = requests.get(documents_url, headers=headers, params=params) response.raise_for_status() data = response.json() documents.extend(data["results"]) log.info( f"Fetched {len(data['results'])} documents, " f"page {data['page']} of {data['pages']}" ) if not data["next"]: break params["offset"] = params.get("offset", 0) + data["limit"] log.info(f"Retrieved {len(documents)} documents from collection {collection_name}") return documents def check_documents(documents: List[Dict[str, Any]]) -> List[str]: """Analyze the processing status of documents and return a list of filenames. Args: documents (List[Dict[str, Any]]): A list of documents in JSON format. Returns: List[str]: A list of filenames from documents with a successful processing status. Raises: None, but logs warnings if a document's processing status is not 'success'. Example: >>> docs = [{"properties": {"processingStatus": ["success"], "fileName": ["file1.txt"]}}, >>> {"properties": {"processingStatus": ["failed"], "fileName": ["file2.txt"]}}] >>> filenames = check_documents(docs) >>> print(filenames) ['file1.txt'] """ filenames = [] for doc in documents: status = doc.get("properties", {}).get("processingStatus")[0] filename = doc.get("properties", {}).get("fileName")[0] if status != "success": log.warning( f"Document with filename {filename} has processing status: {status}" ) if filename: filenames.append(filename) log.debug(f"Collected filenames: {filenames}") return filenames if __name__ == "__main__": app()
Get your API key
By default, any Aleph search will return only public documents in responses to API requests.
If you want to access documents which are not marked public, you will need to sign into the tool. This can be done through the use on an API key. The API key for any account can be found by clicking on the "Settings" menu item in the navigation menu.
Usage
You can run the script directly from the command line. Below are examples of usage:
Retrieve and list documents from a collection:
python list_aleph_collection_documents.py --api-key "your-api-key" 'Name of your collection'
Using an Environment Variable for the API Key
This is better from a security perspective.
export API_KEY=your_api_key python list_aleph_collection_documents.py 'Name of your collection'
Enabling Verbose Logging
To enable detailed debug logs, use the
--verbose
or-v
flag:python list_aleph_collection_documents.py -v 'Name of your collection'
Getting help
python list_aleph_collection_documents.py --help
-
memorious⚑
-
New: Switch to the previous opened buffer.
Often the buffer that you want to edit is the buffer that you have just left. Vim provides a couple of convenient commands to switch back to the previous buffer. These are
<C-^>
(or<C-6>
) and:b#
. All of them are inconvenient so I use the next mapping:nnoremap <Leader><Tab> :b#<CR>
-
New: Troubleshoot Undefined global
vim
warning.Added to my lua/plugins directory:
{ "neovim/nvim-lspconfig", opts = { servers = { lua_ls = { settings = { Lua = { diagnostics = { globals = { "vim" }, }, }, }, }, }, }, },
-
New: Get the version of the packages installed by Packer.
Go into the plugin directory
cd ~/.local/share/nvim/site/pack/packer/start/your-plugin
and check it with git -
New: Introduce memorious.
Memorious is a light-weight web scraping toolkit. It supports scrapers that collect structured or un-structured data. This includes the following use cases:
- Make crawlers modular and simple tasks re-usable
- Provide utility functions to do common tasks such as data storage, HTTP session management
- Integrate crawlers with the Aleph and FollowTheMoney ecosystem
References
Data orchestrators⚑
-
Correction: Update disable regular login with oauth.
The last
signin_inner.tmpl
failed with the latest version. I've uploaded the new working one. -
New: Configure vim to work with markdown.
Markdown specific plugins:
- mkdnflow looks awesome.
-
New: Enable folds.
If you have set the
foldmethod
toindent
by default you won't be able to use folds in markdown.To fix this you can create the next autocommand (in
lua/config/autocmds.lua
if you're using lazyvim).vim.api.nvim_create_autocmd("FileType", { pattern = "markdown", callback = function() vim.wo.foldmethod = "expr" vim.wo.foldexpr = "v:lua.vim.treesitter.foldexpr()" end, })
-
New: Aligning tables in markdown.
In the past I used Tabular but it doesn't work with the latest neovim and the project didn't have any update in the last 5 years.
A good way to achieve this without installing any plugin is to:
- select the table, including the header and footer lines (with shift V, for example).
- Prettify the table with
:!column -t -s '|' -o '|'
If you don't want to remember that command you can bind it to a key:
vim.keymap.set("v", "<leader>tf", "!column -t -s '|' -o '|'<cr>", { desc = "Format table" })
How the hell this works?
shift V
switches to Visual mode linewise. This is to select all the lines of the table.:
switches to Command line mode, to type commands.!
specifies a filter command. This means we will send data to a command to modify it (or to filter) and replace the original lines. In this case we are in Visual mode, we defined the input text (the selected lines) and we will use an external command to modify the data.column
is the filter command we are using, from theutil-linux
package. column’s purpose is to “columnate”. The-t
flag tells column to use the Table mode. The-s
flag specifies the delimiters in the input data (the default is whitespace). And the-o
flag is to specify the output delimiter to use (we need that because the default is two whitespaces).
-
New: Fix Server does not allow request for unadvertised object error.
Fetching the whole history with fetch-depth: 0 worked for us:
- name: Checkout the codebase uses: actions/checkout@v3 with: fetch-depth: 0
-
New: Introduce data orchestrators.
Data orchestration is the process of moving siloed data from multiple storage locations into a centralized repository where it can then be combined, cleaned, and enriched for activation.
Data orchestrators are web applications that make this process easy. The most popular right now are:
- Apache Airflow
- Kestra
- Prefect
There are several comparison pages:
When looking at the return on investment when choosing an orchestration tool, there are several points to consider:
- Time of installation/maintenance
- Time to write pipeline
- Time to execute (performance)
Pros:
- Easier to write pipelines
- Nice looking web UI
- It has a terraform provider
- Prometheus and grafana integration
Cons:
- Built in Java, so extending it might be difficult
- Plugins are made in Java
Kestra offers a higher ROI globally compared to Airflow:
- Installing Kestra is easier than Airflow; it doesn’t require Python dependencies, and it comes with a ready-to-use docker-compose file using few services and without the need to understand what’s an executor to run task in parallel.
- Creating pipelines with Kestra is simple, thanks to its syntax. You don’t need knowledge of a specific programming language because Kestra is designed to be agnostic. The declarative YAML design makes Kestra flows more readable compared to Airflow’s DAG equivalent, allowing developers to significantly reduce development time.
- In this benchmark, Kestra demonstrates better execution time than Airflow under any configuration setup.
Scrapers⚑
-
New: Introduce Debug Adapter Protocol.
nvim-dap
](https://github.com/mfussenegger/nvim-dap) implements a client for the Debug Adapter Protocol. This allows a client to control a debugger over a documented API. That allows us to control the debugger from inside neovim, being able to set breakpoints, evaluate runtime values of variables, and much more.nvim-dap
is not configured for any language by default. You will need to set up a configuration for each language. For the configurations you will need adapters to run.I would suggest starting with 2 actions. Setting breakpoints and “running” the debugger. The debugger allows us to stop execution and look at the current state of the program. Setting breakpoints will allow us to stop execution and see what the current state is.
vim.api.nvim_set_keymap('n', '<leader>b', [[:lua require"dap".toggle_breakpoint()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>c', [[:lua require"dap".continue()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>n', [[:lua require"dap".step_over()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>N', [[:lua require"dap".step_into()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<F5>', [[:lua require"osv".launch({port = 8086})<CR>]], { noremap = true })
Go to a line where a conditional or value is set and toggle a breakpoint. Then, we’ll start the debugger. If done correctly, you’ll see an arrow next to your line of code you set a breakpoint at.
There is no UI with dap by default. You have a few options for UI nvim-dap-ui
In the
dap
repl you can use the next operations:.exit
: Closes the REPL.c
or.continue
: Same as |dap.continue
|.n
or.next
: Same as |dap.step_over
|.into
: Same as |dap.step_into
|.into_target
: Same as |dap.step_into{askForTargets=true}
|.out
: Same as |dap.step_out
|.up
: Same as |dap.up
|.down
: Same as |dap.down
|.goto
: Same as |dap.goto_
|.scopes
: Prints the variables in the current scope
s.threads
: Prints all thread
s.frames
: Print the stack frame
s.capabilities
: Print the capabilities of the debug adapte
r.b
or.back
: Same as |dap.step_back
|.rc
or.reverse-continue
: Same as |dap.reverse_continue
|
-
Install with packer:
use { "rcarriga/nvim-dap-ui", requires = {"mfussenegger/nvim-dap"} }
It is highly recommended to use
neodev.nvim
to enable type checking fornvim-dap-ui
to get type checking, documentation and autocompletion for all API functions.require("neodev").setup({ library = { plugins = { "nvim-dap-ui" }, types = true }, ... })
nvim-dap-ui
is built on the idea of "elements". These elements are windows which provide different features.Elements are grouped into layouts which can be placed on any side of the screen. There can be any number of layouts, containing whichever elements desired.
Elements can also be displayed temporarily in a floating window.
Each element has a set of mappings for element-specific possible actions, detailed below for each element. The total set of actions/mappings and their default shortcuts are:
- edit:
e
- expand:
<CR>
or left click - open:
o
- remove:
d
- repl:
r
- toggle:
t
See
:h dapui.setup()
for configuration options and defaults.To get started simply call the setup method on startup, optionally providing custom settings.
require("dapui").setup()
You can open, close and toggle the windows with corresponding functions:
require("dapui").open() require("dapui").close() require("dapui").toggle()
- edit:
-
New: Debug neovim plugins with DAP.
one-small-step-for-vimkind
is an adapter for the Neovim lua language. It allows you to debug any lua code running in a Neovim instance.Install it with Packer:
After installing one-small-step-for-vimkind, you will also need a DAP plugin which will allow you to interact with the adapter. Check the install instructions here.use 'mfussenegger/nvim-dap'
Then add these lines to your config:
local dap = require"dap" dap.configurations.lua = { { type = 'nlua', request = 'attach', name = "Attach to running Neovim instance", } } dap.adapters.nlua = function(callback, config) callback({ type = 'server', host = config.host or "127.0.0.1", port = config.port or 8086 }) end
-
New: Debugging with DAP.
You can debug Lua code running in a separate Neovim instance with jbyuki/one-small-step-for-vimkind.
The plugin uses the Debug Adapter Protocol. Connecting to a debug adapter requires a DAP client like mfussenegger/nvim-dap. Check how to configure here
Once you have all set up and assuming you're using the next keybindings for
nvim-dap
:vim.api.nvim_set_keymap('n', '<leader>b', [[:lua require"dap".toggle_breakpoint()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>c', [[:lua require"dap".continue()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>n', [[:lua require"dap".step_over()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>N', [[:lua require"dap".step_into()<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<F5>', [[:lua require"osv".launch({port = 8086})<CR>]], { noremap = true }) vim.api.nvim_set_keymap('n', '<leader>B', [[:lua require"dapui".toggle()<CR>]], { noremap = true })
You will debug the plugin by:
- Launch the server in the debuggee using
F5
. - Open another Neovim instance with the source file (the debugger).
- Place breakpoint with
<leader>b
. - On the debugger connect to the DAP client with
<leader>c
. - Optionally open the
nvim-dap-ui
with<leader>B
in the debugger. - Run your script/plugin in the debuggee
- Interact in the debugger using
<leader>n
to step to the next step, and<leader>N
to step into. Then use the dap console to inspect and change the values of the state.
- Launch the server in the debuggee using
-
New: Introduce morph.io.
morph.io is a web service that runs your scrapers for you.
Write your scraper in the language you know and love, push your code to GitHub, and they take care of the boring bits. Things like running your scraper regularly, alerting you if there's a problem, storing your data, and making your data available for download or through a super-simple API.
To sign in you'll need a GitHub account. This is where your scraper code is stored.
The data is stored in an sqlite
Usage limits
Right now there are very few limits. They are trusting you that you won't abuse this.
However, they do impose a couple of hard limits on running scrapers so they don't take up too many resources
- max 512 MB memory
- max 24 hours run time for a single run
If a scraper runs out of memory or runs too long it will get killed automatically.
There's also a soft limit:
- max 10,000 lines of log output
If a scraper generates more than 10,000 lines of log output the scraper will continue running uninterrupted. You just won't see any more output than that. To avoid this happening simply print less stuff to the screen.
Note that they are keeping track of the amount of cpu time (and a whole bunch of other metrics) that you and your scrapers are using. So, if they do find that you are using too much they reserve the right to kick you out. In reality first they'll ask you nicely to stop.
References
Vim autosave⚑
-
Correction: Search for alternatives to git-sweep.
The tool is no longer maintained but there is still no good alternative. I've found some but are either not popular and/or not maintained:
-
New: Update all git submodules.
If it's the first time you check-out a repo you need to use
--init
first:git submodule update --init --recursive
To update to latest tips of remote branches use:
git submodule update --recursive --remote
-
New: Manually toggle the autosave function.
Besides running auto-save at startup (if you have
enabled = true
in your config), you may as well:ASToggle
: toggle auto-save
Espanso⚑
-
New: Introduce espanso.
Espanso is a cross-platform Text Expander written in Rust.
A text expander is a program that detects when you type a specific keyword and replaces it with something else. This is useful in many ways:
- Save a lot of typing, expanding common sentences or fixing common typos.
- Create system-wide code snippets.
- Execute custom scripts
- Use emojis like a pro.
Installation Espanso ships with a .deb package, making the installation convenient on Debian-based systems.
Start by downloading the package by running the following command inside a terminal:
wget https://github.com/federico-terzi/espanso/releases/download/v2.2.1/espanso-debian-x11-amd64.deb
You can now install the package using:
sudo apt install ./espanso-debian-x11-amd64.deb
From now on, you should have the
espanso
command available in the terminal (you can verify by runningespanso --version
).At this point, you are ready to use
espanso
by registering it first as a Systemd service and then starting it with:espanso service register
Start espanso
espanso start
Espanso ships with very few built-in matches to give you the maximum flexibility, but you can expand its capabilities in two ways: creating your own custom matches or installing packages.
Your configuration lives at
~/.config/espanso
. A quick way to find the path of your configuration folder is by using the following commandespanso path
.- The files contained in the
match
directory define what Espanso should do. In other words, this is where you should specify all the custom snippets and actions (aka Matches). Thematch/base.yml
file is where you might want to start adding your matches. - The files contained in the
config
directory define how Espanso should perform its expansions. In other words, this is were you should specify all Espanso's parameters and options. Theconfig/default.yml
file defines the options that will be applied to all applications by default, unless an app-specific configuration is present for the current app.
Custom matches are great, but sometimes it can be tedious to define them for every common operation, especially when you want to share them with other people.
Espanso offers an easy way to share and reuse matches with other people, packages. In fact, they are so important that Espanso includes a built-in package manager and a store, the Espanso Hub.
Get the id of the package from the Espanso Hub and then run
espanso install <<package_name>>
.Of all the packages, I've found the next ones the most useful:
Overwriting the snippets of a package
For example the
typofixer-en
replacessi
tois
, althoughsi
is a valid spanish word. To override the fix you can create your own file on~/.config/espanso/match/typofix_overwrite.yml
with the next content:matches: # Simple text replacement - trigger: "si" replace: "si"
Auto-restart on config changes
Set
auto_restart: true
on~/.config/espanso/config/default.yml
.Changing the search bar shortcut
If the default search bar shortcut conflicts with your i3 configuration set it with:
search_shortcut: CTRL+SHIFT+e
You can hide the notifications by adding the following option to your
$CONFIG/config/default.yml
config:show_notifications: false
Usage
Just type and you'll see the text expanded.
You can use the search bar if you don't remember your snippets.
-
New: Desktop application to add words easily.
Going into the espanso config files to add words is cumbersome, to make things easier you can use the
espansadd
Python script.I'm going to assume that you have the following prerequisites:
- A Linux distribution with i3 window manager installed.
- Python 3 installed.
- Espanso installed and configured.
ruyaml
andtkinter
Python libraries installed.notify-send
installed.- Basic knowledge of editing configuration files in i3.
Installation
Create a new Python script named
espansadd.py
with the following content:import tkinter as tk from tkinter import simpledialog import traceback import subprocess import os import sys from ruyaml import YAML from ruyaml.scanner import ScannerError file_path = os.path.expanduser("~/.config/espanso/match/typofixer_overwrite.yml") def append_to_yaml(file_path: str, trigger: str, replace: str) -> None: """Appends a new entry to the YAML file. Args:ath file_path (str): The file to append the new entry. trigger (str): The trigger string to be added. replace (str): The replacement string to be added. """ # Define the new snippet new_entry = { "trigger": trigger, "replace": replace, "propagate_case": True, "word": True, } # Load existing data or initialize an empty list try: with open(os.path.expanduser(file_path), "r") as f: try: data = YAML().load(f) except ScannerError as e: send_notification( f"Error parsing yaml of configuration file {file_path}", f"{e.problem_mark}: {e.problem}", "critical", ) sys.exit(1) except FileNotFoundError: send_notification( f"Error opening the espanso file {file_path}", urgency="critical" ) sys.exit(1) data["matches"].append(new_entry) # Write the updated data back to the file with open(os.path.expanduser(file_path), "w+") as f: yaml = YAML() yaml.default_flow_style = False yaml.dump(data, f) def send_notification(title: str, message: str = "", urgency: str = "normal") -> None: """Send a desktop notification using notify-send. Args: title (str): The title of the notification. message (str): The message body of the notification. Defaults to an empty string. urgency (str): The urgency level of the notification. Can be 'low', 'normal', or 'critical'. Defaults to 'normal'. """ subprocess.run(["notify-send", "-u", urgency, title, message]) def main() -> None: """Main function to prompt user for input and append to the YAML file.""" # Create the main Tkinter window (it won't be shown) window = tk.Tk() window.withdraw() # Hide the main window # Prompt the user for input trigger = simpledialog.askstring("Espanso add input", "Enter trigger:") replace = simpledialog.askstring("Espanso add input", "Enter replace:") # Check if both inputs were provided try: if trigger and replace: append_to_yaml(file_path, trigger, replace) send_notification("Espanso snippet added successfully") else: send_notification( "Both trigger and replace are required", urgency="critical" ) except Exception as error: error_message = "".join( traceback.format_exception(None, error, error.__traceback__) ) send_notification( "There was an unknown error adding the espanso entry", error_message, urgency="critical", ) if __name__ == "__main__": main()
Ensure the script has executable permissions. Run the following command:
chmod +x espansadd.py
To make the
espansadd
script easily accessible, we can configure a key binding in i3 to run the script. Open your i3 configuration file, typically located at~/.config/i3/config
or~/.i3/config
, and add the following lines:bindsym $mod+Shift+e exec --no-startup-id /path/to/your/espansadd.py
Replace
/path/to/your/espansadd.py
with the actual path to your script.If you also want the popup windows to be in floating mode add
for_window [title="Espanso add input"] floating enable
After editing the configuration file, reload i3 to apply the changes. You can do this by pressing
Mod
+Shift
+R
(whereMod
is typically theSuper
orWindows
key) or by running the following command:i3-msg reload
Usage
Now that everything is set up, you can use the
espansadd
script by pressingMod
+Shift
+E
. This will open a dialog where you can enter the trigger and replacement text for the new Espanso snippet. After entering the information and pressing Enter, a notification will appear confirming the snippet has been added, or showing an error message if something went wrong.
Generic Coding Practices⚑
Writing good documentation⚑
-
New: Add diátaxis as documentation writing guideline.
Diátaxis: A systematic approach to technical documentation authoring
Conventional comments⚑
-
New: Introduce conventional comments.
Conventional comments is the practice to use a specific format in the review comments to express your intent and tone more clearly. It's strongly inspired by semantic versioning.
Let's take the next comment:
This is not worded correctly.
Adding labels you can tell the difference on your intent:
Or**suggestion:** This is not worded correctly.
**issue (non-blocking):** This is not worded correctly.
Labels also prompt the reviewer to give more actionable comments.
**suggestion:** This is not worded correctly. Can we change this to match the wording of the marketing page?
Labeling comments encourages collaboration and saves hours of undercommunication and misunderstandings. They are also parseable by machines!
Format
Adhering to a consistent format improves reader's expectations and machine readability. Here's the format we propose:
- label - This is a single label that signifies what kind of comment is being left. - subject - This is the main message of the comment. - decorations (optional) - These are extra decorating labels for the comment. They are surrounded by parentheses and comma-separated. - discussion (optional) - This contains supporting statements, context, reasoning, and anything else to help communicate the "why" and "next steps" for resolving the comment. For example:<label> [decorations]: <subject> [discussion]
**question (non-blocking):** At this point, does it matter which thread has won? Maybe to prevent a race condition we should keep looping until they've all won?
Can be automatically parsed into:
Labels{ "label": "question", "subject": "At this point, does it matter which thread has won?", "decorations": ["non-blocking"], "discussion": "Maybe to prevent a race condition we should keep looping until they've all won?" }
We strongly suggest using the following labels: | | | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | praise: | Praises highlight something positive. Try to leave at least one of these comments per review. Do not leave false praise (which can actually be damaging). Do look for something to sincerely praise. | | quibble: | Quibbles are trivial preference-based requests. These should be non-blocking by nature. Similar to
polish
but clearly preference-based.| | suggestion: | Suggestions propose improvements to the current subject. It's important to be explicit and clear on what is being suggested and why it is an improvement. These are non-blocking proposals. If it's blocking usetodo
instead.| | todo: | TODO's are necessary changes. Distinguishingtodo
comments fromissues
orsuggestions
helps direct the reader's attention to comments requiring more involvement. | | issue: | Issues highlight specific problems with the subject under review. These problems can be user-facing or behind the scenes. It is strongly recommended to pair this comment with asuggestion
. If you are not sure if a problem exists or not, consider leaving aquestion
. | | question: | Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution. | | thought: | Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities. | | chore: | Chores are simple tasks that must be done before the subject can be "officially" accepted. Usually, these comments reference some common process. Try to leave a link to the process description so that the reader knows how to resolve the chore. | | note: | Notes are always non-blocking and simply highlight something the reader should take note of. |If you like to be a bit more expressive with your labels, you may also consider:
typo: Typo comments are like todo:, where the main issue is a misspelling. polish: Polish comments are like a suggestion, where there is nothing necessarily wrong with the relevant content, there's just some ways to immediately improve the quality. Similar but not exactly the same as quibble
.Decorations
Decorations give additional context for a comment. They help further classify comments which have the same label (for example, a security suggestion as opposed to a test suggestion).
**suggestion (security):** I'm a bit concerned that we are implementing our own DOM purifying function here... Could we consider using the framework instead?
**suggestion (test,if-minor):** It looks like we're missing some unit test coverage that the cat disappears completely.
Decorations may be specific to each organization. If needed, we recommend establishing a minimal set of decorations (leaving room for discretion) with no ambiguity. Possible decorations include: | | | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | (non-blocking) | A comment with this decoration should not prevent the subject under review from being accepted. This is helpful for organizations that consider comments blocking by default. | | (blocking) | A comment with this decoration should prevent the subject under review from being accepted, until it is resolved. This is helpful for organizations that consider comments non-blocking by default. | | (if-minor) | This decoration gives some freedom to the author that they should resolve the comment only if the changes ends up being minor or trivial. |
Adding a decoration to a comment should improve understandability and maintain readability. Having a list of many decorations in one comment would conflict with this goal.
More examples
**quibble:** `little star` => `little bat` Can we update the other references as well?
**chore:** Let's run the `jabber-walk` CI job to make sure this doesn't break any known references. Here are [the docs](https://en.wikipedia.org/wiki/Jabberwocky) for running this job. Feel free to reach out if you need any help!
**praise:** Beautiful test!
Best Practices
Some best practices for writing helpful review feedback:
- Mentoring pays off exponentially
- Leave actionable comments
- Combine similar comments
- Replace "you" with "we"
- Replace "should" with "could"
References - Home
OCR⚑
Camelot⚑
-
New: Introduce Camelot.
Camelot is a Python library that can help you extract tables from PDFs
import camelot tables = camelot.read_pdf('foo.pdf') tables <TableList n=1> tables.export('foo.csv', f='csv', compress=True) # json, excel, html, markdown, sqlite tables[0] <Table shape=(7, 7)> tables[0].parsing_report { 'accuracy': 99.02, 'whitespace': 12.24, 'order': 1, 'page': 1 } tables[0].to_csv('foo.csv') # to_json, to_excel, to_html, to_markdown, to_sqlite tables[0].df # get a pandas DataFrame!
To install Camelot from PyPI using pip, please include the extra cv requirement as shown:
$ pip install "camelot-py[base]"
It requires Ghostscript to be able to use the
lattice
mode. Which is better than usingtabular-py
that requires java to be installed.Usage
To detect line segments, Lattice needs the lines that make the table to be in the foreground. To process background lines, you can pass process_background=True.
tables = camelot.read_pdf('background_lines.pdf', process_background=True)
tables[1].df
References - Docs
DevSecOps⚑
Infrastructure as Code⚑
Ansible Snippets⚑
-
New: Fix the
ERROR! 'become' is not a valid attribute for a IncludeRole
error.If you're trying to do something like:
tasks: - name: "Install nfs" become: true ansible.builtin.include_role: name: nfs
You need to use this other syntax:
tasks: - name: "Install nfs" ansible.builtin.include_role: name: nfs apply: become: true
-
New: Set host variables using a dynamic inventory.
As with a normal inventory you can use the
host_vars
files with the proper name. -
New: Avoid arbitrary disk mount.
Instead of using
/dev/sda
use/dev/disk/by-id/whatever
-
New: Get the user running ansible in the host.
If you
gather_facts
use theansible_user_id
variable. -
New: Filter json data.
To select a single element or a data subset from a complex data structure in JSON format (for example, Ansible facts), use the
community.general.json_query
filter. Thecommunity.general.json_query
filter lets you query a complex JSON structure and iterate over it using a loop structure.This filter is built upon jmespath, and you can use the same syntax. For examples, see jmespath examples.
A complex example would be:
"{{ ec2_facts | json_query('instances[0].block_device_mappings[?device_name!=`/dev/sda1` && device_name!=`/dev/xvda`].{device_name: device_name, id: ebs.volume_id}') }}"
This snippet:
- Gets all dictionaries under the block_device_mappings list which
device_name
is not equal to/dev/sda1
or/dev/xvda
- From those results it extracts and flattens only the desired values. In this case
device_name
and theid
which is at the keyebs.volume_id
of each of the items of the block_device_mappings list.
- Gets all dictionaries under the block_device_mappings list which
-
New: Do asserts.
- name: After version 2.7 both 'msg' and 'fail_msg' can customize failing assertion message ansible.builtin.assert: that: - my_param <= 100 - my_param >= 0 fail_msg: "'my_param' must be between 0 and 100" success_msg: "'my_param' is between 0 and 100"
-
New: Split a variable in ansible.
{{ item | split ('@') | last }}
-
New: Get a list of EC2 volumes mounted on an instance an their mount points.
Assuming that each volume has a tag
mount_point
you could:- name: Gather EC2 instance metadata facts amazon.aws.ec2_metadata_facts: - name: Gather info on the mounted disks delegate_to: localhost block: - name: Gather information about the instance amazon.aws.ec2_instance_info: instance_ids: - "{{ ansible_ec2_instance_id }}" register: ec2_facts - name: Gather volume tags amazon.aws.ec2_vol_info: filters: volume-id: "{{ item.id }}" # We exclude the root disk as they are already mounted and formatted loop: "{{ ec2_facts | json_query('instances[0].block_device_mappings[?device_name!=`/dev/sda1` && device_name!=`/dev/xvda`].{device_name: device_name, id: ebs.volume_id}') }}" register: volume_tags_data - name: Save the required volume data set_fact: volumes: "{{ volume_tags_data | json_query('results[0].volumes[].{id: id, mount_point: tags.mount_point}') }}" - name: Display volumes data debug: msg: "{{ volumes }}" - name: Make sure that all volumes have a mount point assert: that: - item.mount_point is defined - item.mount_point|length > 0 fail_msg: "Configure the 'mount_point' tag on the volume {{ item.id }} on the instance {{ ansible_ec2_instance_id }}" success_msg: "The volume {{ item.id }} has the mount_point tag well set" loop: "{{ volumes }}"
-
New: Create a list of dictionaries using ansible.
- name: Create and Add items to dictionary set_fact: userdata: "{{ userdata | default({}) | combine ({ item.key : item.value }) }}" with_items: - { 'key': 'Name' , 'value': 'SaravAK'} - { 'key': 'Email' , 'value': 'sarav@gritfy.com'} - { 'key': 'Location' , 'value': 'Coimbatore'} - { 'key': 'Nationality' , 'value': 'Indian'}
-
New: Merge two dictionaries on a key.
If you have these two lists:
And want to merge them using the value of key "a":"list1": [ { "a": "b", "c": "d" }, { "a": "e", "c": "f" } ] "list2": [ { "a": "e", "g": "h" }, { "a": "b", "g": "i" } ]
"list3": [ { "a": "b", "c": "d", "g": "i" }, { "a": "e", "c": "f", "g": "h" } ]
If you can install the collection community.general use the filter lists_mergeby. The expression below gives the same result
list3: "{{ list1|community.general.lists_mergeby(list2, 'a') }}"
-
New: Loop over dict fails when only one element detected.
If you see the
If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup."
error in an Ansible log it means that the content of the variable is not the type you expect it to be. This can happen for example for lists that have only one or zero elements, which gets translated into a string thus breaking theloop
structure.So instead of:
- name: Create filesystem on device community.general.filesystem: fstype: ext4 dev: "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol{{ item.id | split('-') | last }}" loop: "{{ volumes }}"
You can use:
- name: Create filesystem on device community.general.filesystem: fstype: ext4 dev: "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol{{ item.id | split('-') | last }}" loop: "{{ lookup('list', volumes, wantlist=True) }}"
If that gives you issues you can use this other construction instead:
- name: Save the required volume data set_fact: volumes: "{{ volume_tags_data | json_query('results[0].volumes[].{id: id, mount_point: tags.mount_point}') }}" - name: Get result type for the volumes set_fact: volumes_type: "{{ volumes | type_debug }}" - name: Display volumes type debug: msg: "{{ volumes_type }}" - name: Force list of volumes if it's a string set_fact: volumes: "{{ [] }}" when: - volumes_type == 'str' - name: Force list of volumes if it's a dictionary set_fact: volumes: "{{ [volumes] }}" when: - volumes_type == 'dict' - name: Create filesystem on device community.general.filesystem: fstype: ext4 dev: "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol{{ item.id | split('-') | last }}" loop: "{{ volumes }}"
-
New: Set the ssh connection port using dynamic inventories.
To specify a custom SSH port, you can use a
host_vars
orgroup_vars
file. For example, create agroup_vars
directory and a file namedall.yaml
inside it:ansible_port: 2222
Infrastructure Solutions⚑
Kubernetes⚑
-
New: Introduce IceKube.
IceKube tool for finding complex attack paths in Kubernetes clusters. It's like Bloodhound for Kubernetes. It uses Neo4j to store & analyze Kubernetes resource relationships → identify attack paths & security misconfigs
-
New: Add reloader to tools to test.
stakater/reloader: A Kubernetes controller to watch changes in ConfigMap and Secrets and do rolling upgrades on Pods with their associated Deployment, StatefulSet, DaemonSet and DeploymentConfig. Useful for not that clever applications that need a reboot when a configmap changes.
AWS⚑
Kubectl Commands⚑
-
New: Understanding how reserved instances are applied.
A Reserved Instance that is purchased for a Region is called a regional Reserved Instance, and provides Availability Zone and instance size flexibility.
- The Reserved Instance discount applies to instance usage in any Availability Zone in that Region.
- The Reserved Instance discount applies to instance usage within the instance family, regardless of size—this is known as instance size flexibility.
With instance size flexibility, the Reserved Instance discount applies to instance usage for instances that have the same family, generation, and attribute. The Reserved Instance is applied from the smallest to the largest instance size within the instance family based on the normalization factor.
The discount applies either fully or partially to running instances of the same instance family, depending on the instance size of the reservation, in any Availability Zone in the Region. The only attributes that must be matched are the instance family, tenancy, and platform.
The following table lists the different sizes within an instance family, and the corresponding normalization factor. This scale is used to apply the discounted rate of Reserved Instances to the normalized usage of the instance family.
Instance size Normalization factor nano 0.25 micro 0.5 small 1 medium 2 large 4 xlarge 8 2xlarge 16 3xlarge 24 4xlarge 32 6xlarge 48 8xlarge 64 9xlarge 72 10xlarge 80 12xlarge 96 16xlarge 128 18xlarge 144 24xlarge 192 32xlarge 256 48xlarge 384 56xlarge 448 112xlarge 896 For example, a
t2.medium
instance has a normalization factor of2
. If you purchase at2.medium
default tenancy Amazon Linux/Unix Reserved Instance in the US East (N. Virginia) and you have two runningt2.small
instances in your account in that Region, the billing benefit is applied in full to both instances.Or, if you have one
t2.large
instance running in your account in the US East (N. Virginia) Region, the billing benefit is applied to 50% of the usage of the instance.Limitations:
- Supported: Instance size flexibility is only supported for Regional Reserved Instances.
- Not supported: Instance size flexibility is not supported for the following Reserved Instances:
- Reserved Instances that are purchased for a specific Availability Zone (zonal Reserved Instances)
- Reserved Instances for G4ad, G4dn, G5, G5g, and Inf1 instances
- Reserved Instances for Windows Server, Windows Server with SQL Standard, Windows Server with SQL Server Enterprise, Windows Server with SQL Server Web, RHEL, and SUSE Linux Enterprise Server
- Reserved Instances with dedicated tenancy
-
New: EC2 Instance savings plan versus reserved instances.
I've been comparing the EC2 Reserved Instances and of the EC2 instance family savings plans and decided to go with the second because:
- They both have almost the same rates. Reserved instances round the price at the 3rd decimal and the savings plan at the fourth, but this difference is neglegible.
- Savings plan are easier to calculate, as you just need to multiply the number of instances you want times the current rate and add them all up.
- Easier to understand: To reserve instances you need to take into account the instance flexibility and the normalization factors which makes it difficult both to make the plans and also to audit how well you're using it.
- Easier to audit: In addition to the above point, you have nice dashboards to see the coverage and utilization over time of your ec2 instance savings plans, which are at the same place as the other savings plans.
-
New: Important notes when doing a savings plan.
- Always use the reservation rates instead of the on-demand rates!
- Analyze your coverage reports. You don't want to have many points of 100% coverage as it means that you're using less resources than you've reserved. On the other hand it's fine to sometimes use less resources than the reserved if that will mean a greater overall savings. It's a tight balance.
- The Savings plan reservation is taken into account at hour level, not at month or year level. That means that if you reserve 1/hour of an instance type and you use for example 2/hour half the day and 0/hour half the day, you'll have a 100% coverage of your plan the first hour and another 1/hour of on-demand infrastructure cost for the first part of the day. On the second part of the day you'll have a 0% coverage. This means that you should only reserve the amount of resources you plan to be using 100% of the time throughout your savings plan. Again you may want to overcommit a little bit, reducing the utilization percentage of a plan but getting better savings in the end.
-
kubectl cp {{ path_to_local_file }} {{ container_id }}:{{ path_to_file }}
-
New: Delete pods that are stuck in terminating state for a while.
kubectl delete pod <pod-name> --grace-period=0 --force
Volumes⚑
-
New: Troubleshoot don't have enough permissions to start restore from backup.
That may be because the default EFS backup policy doesn't let you do that (stupid, I know).
To fix it go into the backup policy and remove the next line from the
Deny
policy:backup:StartRestoreJob
-
New: Specify a path of a configmap.
If you have a configmap with a key
ssh-known-hosts
and you want to mount it's content in a file, in the deploymentvolumeMounts
section you can use thesubPath
field:- mountPath: /home/argocd/.ssh/known_hosts name: ssh-known-hosts subPath: ssh_known_hosts readOnly: true
-
New: List the size of the recovery points.
BACKUP_VAULT_NAME="your-vault-name" RECOVERY_POINTS=$(aws backup list-recovery-points-by-backup-vault --backup-vault-name $BACKUP_VAULT_NAME --query 'RecoveryPoints[*].[RecoveryPointArn,BackupSizeInBytes,CreationDate]' --output text) echo -e "Creation Date\t\tRecovery Point ARN\t\t\t\t\t\t\t\t\tSize (TB)" echo "---------------------------------------------------------------------------------------------------------------------" while read -r RECOVERY_POINT_ARN BACKUP_SIZE_BYTES CREATION_DATE; do # Remove the decimal part from the epoch time EPOCH_TIME=$(echo $CREATION_DATE | cut -d'.' -f1) # Convert the creation date from epoch time to YYYY-MM-DD format FORMATTED_DATE=$(date -d @$EPOCH_TIME +"%Y-%m-%d") SIZE_TB=$(echo "scale=6; $BACKUP_SIZE_BYTES / (1024^4)" | bc) # echo -e "$FORMATTED_DATE\t$RECOVERY_POINT_ARN\t$SIZE_TB" printf "%-16s %-80s %10.6f\n" "$FORMATTED_DATE" "$RECOVERY_POINT_ARN" "$SIZE_TB" done <<< "$RECOVERY_POINTS"
-
New: List the size of the jobs.
To list AWS Backup jobs and display their completion dates and sizes in a human-readable format, you can use the following AWS CLI command combined with
jq
for parsing and formatting the output. This command handles cases where the backup size might be null and rounds the size to the nearest whole number in gigabytes.Explanation:aws backup list-backup-jobs --output json | jq -r ' .BackupJobs[] | [ (.CompletionDate | strftime("%Y-%m-%d %H:%M:%S")), (if .BackupSizeInBytes == null then "0GB" else ((.BackupSizeInBytes / 1024 / 1024 / 1024) | floor | tostring + " GB") end) ] | @tsv' | column -t -s$'\t'
aws backup list-backup-jobs --output json
: Lists all AWS Backup jobs in JSON format..BackupJobs[]
: Iterates over each backup job.(.CompletionDate | strftime("%Y-%m-%d %H:%M:%S"))
: Converts the Unix timestamp in CompletionDate to a human-readable date format (YYYY-MM-DD HH:MM:SS).(if .BackupSizeInBytes == null then "0GB" else ((.BackupSizeInBytes / 1024 / 1024 / 1024) | floor | tostring + " GB") end)
: Checks if BackupSizeInBytes is null. If it is, outputs "0GB". Otherwise, converts the size from bytes to gigabytes, rounds it down to the nearest whole number, and appends " GB".| @tsv
: Formats the output as tab-separated values.column -t -s$'\t'
: Formats the TSV output into a table with columns aligned.
Continuous Deployment⚑
ArgoCD⚑
-
New: Not there yet.
- Support git webhook on Applicationsets for gitea/forgejo: although you could use an ugly fix adding
spec.generators[i].requeueAfterSeconds
to change the interval that ArgoCD uses to refresh the repositories, which is 3 minutes by default.
- Support git webhook on Applicationsets for gitea/forgejo: although you could use an ugly fix adding
Continuous Integration⚑
Bandit⚑
-
New: Solving warning B603: subprocess_without_shell_equals_true.
The
B603: subprocess_without_shell_equals_true
issue in Bandit is raised when thesubprocess
module is used without settingshell=True
. Bandit flags this because usingshell=True
can be a security risk if the command includes user-supplied input, as it opens the door to shell injection attacks.To fix it:
- Avoid
shell=True
if possible: Instead, pass the command and its arguments as a list tosubprocess.Popen
(orsubprocess.run
,subprocess.call
, etc.). This way, the command is executed directly without invoking the shell, reducing the risk of injection attacks.
Here's an example:
import subprocess # Instead of this: # subprocess.Popen("ls -l", shell=True) # Do this: subprocess.Popen(["ls", "-l"])
- When you must use
shell=True
: - If you absolutely need to useshell=True
(e.g., because you are running a complex shell command or using shell features like wildcards), ensure that the command is either hardcoded or sanitized to avoid security risks.
Example with
shell=True
:import subprocess # Command is hardcoded and safe command = "ls -l | grep py" subprocess.Popen(command, shell=True)
If the command includes user input, sanitize the input carefully:
import subprocess user_input = "some_directory" command = f"ls -l {subprocess.list2cmdline([user_input])}" subprocess.Popen(command, shell=True)
Note: Even with precautions, using
shell=True
is risky with user input, so avoid it if possible.- Explicitly tell bandit you have considered the risk: If you have reviewed the code and are confident that the code is safe in your particular case, you can mark the line with a
# nosec
comment to tell Bandit to ignore the issue:
import subprocess command = "ls -l | grep py" subprocess.Popen(command, shell=True) # nosec
- Avoid
Safety⚑
-
New: Add deprecation warning.
Since 2024-05-27 it requires an account to work, use pip-audit instead.
Security Checkers⚑
-
New: Introduce pip-audit.
pip-audit
is the official pypa tool for scanning Python environments for packages with known vulnerabilities. It uses the Python Packaging Advisory Database (https://github.com/pypa/advisory-database) via the PyPI JSON API as a source of vulnerability reports.Installation
pip install pip-audit
Usage
On completion, pip-audit will exit with a code indicating its status.pip-audit
The current codes are:
0
: No known vulnerabilities were detected.1
: One or more known vulnerabilities were found.
pip-audit's exit code cannot be suppressed. See Suppressing exit codes from pip-audit for supported alternatives.
References
Yamllint⚑
-
It is possible to exclude specific files or directories, so that the linter doesn’t process them. They can be provided either as a list of paths, or as a bulk string.
You can either totally ignore files (they won’t be looked at):
extends: default ignore: | /this/specific/file.yaml all/this/directory/ *.template.yaml ignore: - /this/specific/file.yaml - all/this/directory/ - '*.template.yaml'
Automating Processes⚑
letsencrypt⚑
-
New: Manually renew a certificate.
Linuxserver swag container renews the certificates at night. If you don't have your server up at those hours your certificate won't be renewed automatically and you need to react to the prometheus alert manually. To do so get into the container and run
certbot renew
.
Storage⚑
NAS⚑
-
Correction: Add suggestions when buying a motherboard.
When choosing a motherboard make sure that:
- If you want ECC that it truly supports ECC.
- It is IPMI compliant, if you want to have hardware watchdog support
OpenZFS⚑
-
New: Solve the pool or dataset is busy error.
If you get an error of
pool or dataset is busy
run the next command to see which process is still running on the pool:lsof 2>/dev/null | grep dataset-name
-
New: Monitor dbgmsg with loki.
If you use loki remember to monitor the
/proc/spl/kstat/zfs/dbgmsg
file:- job_name: zfs static_configs: - targets: - localhost labels: job: zfs __path__: /proc/spl/kstat/zfs/dbgmsg
-
Correction: Add loki alerts on the kernel panic error.
You can monitor this issue with loki using the next alerts:
groups: - name: zfs rules: - alert: SlowSpaSyncZFSError expr: | count_over_time({job="zfs"} |~ `spa_deadman.*slow spa_sync` [5m]) for: 1m labels: severity: critical annotations: summary: "Slow sync traces found in the ZFS debug logs at {{ $labels.hostname}}" message: "This usually happens before the ZFS becomes unresponsible"
-
New: Monitorization.
You can monitor this issue with loki using the next alerts:
groups: - name: zfs rules: - alert: ErrorInSanoidLogs expr: | count_over_time({job="systemd-journal", syslog_identifier="sanoid"} |= `ERROR` [5m]) for: 1m labels: severity: critical annotations: summary: "Errors found on sanoid log at {{ $labels.hostname}}"
-
New: Set zfs module parameters or options.
Most of the ZFS kernel module parameters are accessible in the SysFS
/sys/module/zfs/parameters
directory. Current values can be observed bycat /sys/module/zfs/parameters/PARAMETER
Many of these can be changed by writing new values. These are denoted by Change|Dynamic in the PARAMETER details below.
echo NEWVALUE >> /sys/module/zfs/parameters/PARAMETER
If the parameter is not dynamically adjustable, an error can occur and the value will not be set. It can be helpful to check the permissions for the
PARAMETER
file in SysFS.In some cases, the parameter must be set prior to loading the kernel modules or it is desired to have the parameters set automatically at boot time. For many distros, this can be accomplished by creating a file named
/etc/modprobe.d/zfs.conf
containing a text line for each module parameter using the format:options zfs PARAMETER=VALUE
Some parameters related to ZFS operations are located in module parameters other than in the zfs kernel module. These are documented in the individual parameter description. Unless otherwise noted, the tunable applies to the zfs kernel module. For example, the
icp
kernel module parameters are visible in the/sys/module/icp/parameters
directory and can be set by default at boot time by changing the/etc/modprobe.d/icp.conf
file. -
New: Configure the deadman failsafe measure.
ZFS has a safety measure called the zfs_deadman_failmode. When a pool sync operation takes longer than
zfs_deadman_synctime_ms
, or when an individual I/O operation takes longer thanzfs_deadman_ziotime_ms
, then the operation is considered to be "hung". Ifzfs_deadman_enabled
is set, then the deadman behavior is invoked as described byzfs_deadman_failmode
. By default, the deadman is enabled and set to wait which results in "hung" I/O operations only being logged. The deadman is automatically disabled when a pool gets suspended.zfs_deadman_failmode
configuration can have the next values:wait
: Wait for a "hung" operation to complete. For each "hung" operation a "deadman" event will be posted describing that operation.continue
: Attempt to recover from a "hung" operation by re-dispatching it to the I/O pipeline if possible.panic
: Panic the system. This can be used to facilitate automatic fail-over to a properly configured fail-over partner.
Follow the guides under Set zfs module parameters or options to change this value.
-
You can see the ZFS events using
zpool events -v
. If you want to be alerted on these events you can use this service to ingest them into Loki and raise alerts. -
New: Manually create a backup.
To create a snapshot of
tank/home/ahrens
that is namedfriday
run:zfs snapshot tank/home/ahrens@friday
-
New: Tweak loki alerts.
- alert: SyncoidCorruptedSnapshotSendError expr: | count_over_time({syslog_identifier="syncoid_send_backups"} |= `cannot receive incremental stream: invalid backup stream` [15m]) > 0 for: 0m labels: severity: critical annotations: summary: "Error trying to send a corrupted snapshot at {{ $labels.hostname}}" message: "Look at the context on loki to identify the snapshot in question. Delete it and then run the sync again" - alert: SanoidNotRunningError expr: | sum by (hostname) (count_over_time({job="systemd-journal", syslog_identifier="sanoid"}[1h])) or sum by (hostname) (count_over_time({job="systemd-journal"}[1h]) * 0) for: 0m labels: severity: critical annotations: summary: "Sanoid has not shown signs to be alive for the last hour at least in arva and helm" - alert: SlowSpaSyncZFSError expr: | count_over_time({job="zfs"} |~ `spa_deadman.*slow spa_sync` [10m]) > 0 for: 0m labels: severity: critical annotations: summary: "Slow sync traces found in the ZFS debug logs at {{ $labels.hostname}}" message: "This usually happens before the ZFS becomes unresponsible"
The
SanoidNotRunningError
alert uses a broader search that ensures that all hosts are included and multiplies it to 0 to raise the alert if none is shown for thesanoid
service. -
New: Sync an already created cold backup.
Mount the existent pool
Imagine your pool is at
/dev/sdf2
:- Connect your device
- Check for available ZFS pools: First, check if the system detects any ZFS pools that can be imported:
sudo zpool import
This command will list all pools that are available for import, including the one stored in
/dev/sdf2
. Look for the pool name you want to import.- Import the pool: If you see the pool listed and you know its name (let's say the pool name is
mypool
), you can import it with:
sudo zpool import mypool
- Import the pool from a specific device: If the pool isn't showing up or you want to specify the device directly, you can use:
sudo zpool import -d /dev/sdf2
This tells ZFS to look specifically at
/dev/sdf2
for any pools. If you don't know the name of the pool this is also the command to run.This should list any pools found on the device. If it shows a pool, import it using:
sudo zpool import -d /dev/sdf2 <pool_name>
- Mount the pool: Once the pool is imported, ZFS should automatically mount any datasets associated with the pool. You can check the status of the pool with:
sudo zpool status
Additional options:
- If the pool was exported cleanly, you can use
zpool import
without additional flags. - If the pool wasn’t properly exported or was interrupted, you might need to use
-f
(force) to import it:
feat(linux_snippets#Create a systemd service for a non-root user): Create a systemd service for a non-root usersudo zpool import -f mypool
To set up a systemd service as a non-root user, you can create a user-specific service file under your home directory. User services are defined in
~/.config/systemd/user/
and can be managed without root privileges.- Create the service file:
Open a terminal and create a new service file in
~/.config/systemd/user/
. For example, if you want to create a service for a script namedmy_script.py
, follow these steps:mkdir -p ~/.config/systemd/user nano ~/.config/systemd/user/my_script.service
- Edit the service file:
In the
my_script.service
file, add the following configuration:[Unit] Description=My Python Script Service After=network.target [Service] Type=simple ExecStart=/usr/bin/python3 /path/to/your/script/my_script.py WorkingDirectory=/path/to/your/script/ SyslogIdentifier=my_script Restart=on-failure StandardOutput=journal StandardError=journal [Install] WantedBy=default.target
- Description: A short description of what the service does.
-
ExecStart: The command to run your script. Replace
/path/to/your/script/my_script.py
with the full path to your Python script. If you want to run the script within a virtualenv you can use/path/to/virtualenv/bin/python
instead of/usr/bin/python3
.You'll need to add the virtualenv path to Path
- WorkingDirectory: Set the working directory to where your script is located (optional). - Restart: Restart the service if it fails. - StandardOutput and StandardError: This ensures that the output is captured in the systemd journal. - WantedBy: Specifies the target to which this service belongs.# Add virtualenv's bin directory to PATH Environment="PATH=/path/to/virtualenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
default.target
is commonly used for user services. -
Reload systemd to recognize the new service:
Run the following command to reload systemd's user service files:
systemctl --user daemon-reload
- Enable and start the service:
To start the service immediately and enable it to run on boot (for your user session), use the following commands:
systemctl --user start my_script.service systemctl --user enable my_script.service
-
Check the status and logs:
-
To check if the service is running:
systemctl --user status my_script.service
-
To view logs specific to your service:
journalctl --user -u my_script.service -f
If you need to use the graphical interface
If your script requires user interaction (like entering a GPG passphrase), it’s crucial to ensure that the service is tied to your graphical user session, which ensures that prompts can be displayed and interacted with.
To handle this situation, you should make a few adjustments to your systemd service:
Ensure service is bound to graphical session
Change the
WantedBy
target tographical-session.target
instead ofdefault.target
. This makes sure the service waits for the full graphical environment to be available.Use
Type=forking
instead ofType=simple
(optional)If you need the service to wait until the user is logged in and has a desktop session ready, you might need to tweak the service type. Usually,
Type=simple
is fine, but you can also experiment withType=forking
if you notice any issues with user prompts.Here’s how you should modify your
mbsync_syncer.service
file:[Unit] Description=My Python Script Service After=graphical-session.target [Service] Type=simple ExecStart=/usr/bin/python3 /path/to/your/script/my_script.py WorkingDirectory=/path/to/your/script/ Restart=on-failure StandardOutput=journal StandardError=journal SyslogIdentifier=my_script Environment="DISPLAY=:0" Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" [Install] WantedBy=graphical-session.target
After modifying the service, reload and restart it:
systemctl --user daemon-reload systemctl --user restart my_script.service
ZFS Prometheus exporter⚑
-
Correction: Tweak the zfs_exporter target not available error.
Remember to set the
scrape_timeout
to at least of60s
as the exporter is sometimes slow to answer, specially on low hardware resources.- job_name: zfs_exporter metrics_path: /metrics scrape_timeout: 60s static_configs: - targets: [192.168.3.236:9134] metric_relabel_configs: ...
-
Correction: Tweak the ZfsPoolUnhealthy alert.
- alert: ZfsPoolUnhealthy expr: last_over_time(zfs_pool_health[1h]) > 0 for: 5m labels: severity: critical
Resilience⚑
-
New: Introduce linux resilience.
Increasing the resilience of the servers is critical when hosting services for others. This is the roadmap I'm following for my servers.
Autostart services if the system reboots Using init system services to manage your services
**Get basic metrics traceability and alerts ** Set up Prometheus with:
- The blackbox exporter to track if the services are available to your users and to monitor SSL certificates health.
- The node exporter to keep track on the resource usage of your machines and set alerts to get notified when concerning events happen (disks are getting filled, CPU usage is too high)
**Get basic logs traceability and alerts **
Set up Loki and clear up your system log errors.
Improve the resilience of your data If you're still using
ext4
for your filesystems instead ofzfs
you're missing a big improvement. To set it up:- Plan your zfs storage architecture
- Install ZFS
- Create ZFS local and remote backups
- [Monitor your ZFS ]
Automatically react on system failures - Kernel panics - watchdog
Future undeveloped improvements - Handle the system reboots after kernel upgrades
Memtest⚑
-
New: Introduce memtest.
memtest86 is a testing software for RAM.
Installation
apt-get install memtest86+
After the installation you'll get Memtest entries in grub which you can spawn.
For some unknown reason the memtest of the boot menu didn't work for me. So I downloaded the latest free version of memtest (It's at the bottom of the screen), burnt it in a usb and booted from there.
Usage It will run by itself. For 64GB of ECC RAM it took aproximately 100 minutes to run all the tests.
Check ECC errors MemTest86 directly polls ECC errors logged in the chipset/memory controller registers and displays it to the user on-screen. In addition, ECC errors are written to the log and report file.
watchdog⚑
-
New: Introduce the watchdog.
A watchdog timer (WDT, or simply a watchdog), sometimes called a computer operating properly timer (COP timer), is an electronic or software timer that is used to detect and recover from computer malfunctions. Watchdog timers are widely used in computers to facilitate automatic correction of temporary hardware faults, and to prevent errant or malevolent software from disrupting system operation.
During normal operation, the computer regularly restarts the watchdog timer to prevent it from elapsing, or "timing out". If, due to a hardware fault or program error, the computer fails to restart the watchdog, the timer will elapse and generate a timeout signal. The timeout signal is used to initiate corrective actions. The corrective actions typically include placing the computer and associated hardware in a safe state and invoking a computer reboot.
Microcontrollers often include an integrated, on-chip watchdog. In other computers the watchdog may reside in a nearby chip that connects directly to the CPU, or it may be located on an external expansion card in the computer's chassis.
Hardware watchdog
Before you start using the hardware watchdog you need to check if your hardware actually supports it.
If you see Watchdog hardware is disabled error on boot things are not looking good.
Check if the hardware watchdog is enabled You can see if hardware watchdog is loaded by running
wdctl
. For example for a machine that has it enabled you'll see:Device: /dev/watchdog0 Identity: iTCO_wdt [version 0] Timeout: 30 seconds Pre-timeout: 0 seconds Timeleft: 30 seconds FLAG DESCRIPTION STATUS BOOT-STATUS KEEPALIVEPING Keep alive ping reply 1 0 MAGICCLOSE Supports magic close char 0 0 SETTIMEOUT Set timeout (in seconds) 0 0
On a machine that doesn't you'll see:
wdctl: No default device is available.: No such file or directory
Another option is to run
dmesg | grep wd
ordmesg | grep watc -i
. For example for a machine that has enabled the hardware watchdog you'll see something like:[ 20.708839] iTCO_wdt: Intel TCO WatchDog Timer Driver v1.11 [ 20.708894] iTCO_wdt: Found a Intel PCH TCO device (Version=4, TCOBASE=0x0400) [ 20.709009] iTCO_wdt: initialized. heartbeat=30 sec (nowayout=0)
For one that is not you'll see:
[ 1.934999] sp5100_tco: SP5100/SB800 TCO WatchDog Timer Driver [ 1.935057] sp5100-tco sp5100-tco: Using 0xfed80b00 for watchdog MMIO address [ 1.935062] sp5100-tco sp5100-tco: Watchdog hardware is disabled
If you're out of luck and your hardware doesn't support it you can delegate the task to the software watchdog or get some usb watchdog
Starting with version 183 systemd provides full support for hardware watchdogs (as exposed in /dev/watchdog to userspace), as well as supervisor (software) watchdog support for invidual system services. The basic idea is the following: if enabled, systemd will regularly ping the watchdog hardware. If systemd or the kernel hang this ping will not happen anymore and the hardware will automatically reset the system. This way systemd and the kernel are protected from boundless hangs -- by the hardware. To make the chain complete, systemd then exposes a software watchdog interface for individual services so that they can also be restarted (or some other action taken) if they begin to hang. This software watchdog logic can be configured individually for each service in the ping frequency and the action to take. Putting both parts together (i.e. hardware watchdogs supervising systemd and the kernel, as well as systemd supervising all other services) we have a reliable way to watchdog every single component of the system.
Configuring the watchdog To make use of the hardware watchdog it is sufficient to set the
RuntimeWatchdogSec=
option in/etc/systemd/system.conf
. It defaults to0
(i.e. no hardware watchdog use). Set it to a value like20s
and the watchdog is enabled. After 20s of no keep-alive pings the hardware will reset itself. Note thatsystemd
will send a ping to the hardware at half the specified interval, i.e. every 10s.Note that the hardware watchdog device (
/dev/watchdog
) is single-user only. That means that you can either enable this functionality in systemd, or use a separate external watchdog daemon, such as the aptly namedwatchdog
. Although the built-in hardware watchdog support of systemd does not conflict with other watchdog software by default. systemd does not make use of/dev/watchdog
by default, and you are welcome to use external watchdog daemons in conjunction with systemd, if this better suits your needs.ShutdownWatchdogSec=`` is another option that can be configured in
/etc/systemd/system.conf`. It controls the watchdog interval to use during reboots. It defaults to 10min, and adds extra reliability to the system reboot logic: if a clean reboot is not possible and shutdown hangs, we rely on the watchdog hardware to reset the system abruptly, as extra safety net.Now, let's have a look how to add watchdog logic to individual services.
First of all, to make software watchdog-supervisable it needs to be patched to send out "I am alive" signals in regular intervals in its event loop. Patching this is relatively easy. First, a daemon needs to read the
WATCHDOG_USEC=
environment variable. If it is set, it will contain the watchdog interval in usec formatted as ASCII text string, as it is configured for the service. The daemon should then issuesd_notify("WATCHDOG=1")
calls every half of that interval. A daemon patched this way should transparently support watchdog functionality by checking whether the environment variable is set and honouring the value it is set to.To enable the software watchdog logic for a service (which has been patched to support the logic pointed out above) it is sufficient to set the
WatchdogSec=
to the desired failure latency. Seesystemd.service(5)
for details on this setting. This causesWATCHDOG_USEC=
to be set for the service's processes and will cause the service to enter a failure state as soon as no keep-alive ping is received within the configured interval.The next step is to configure whether the service shall be restarted and how often, and what to do if it then still fails. To enable automatic service restarts on failure set
Restart=on-failure
for the service. To configure how many times a service shall be attempted to be restarted use the combination ofStartLimitBurst=
andStartLimitInterval=
which allow you to configure how often a service may restart within a time interval. If that limit is reached, a special action can be taken. This action is configured withStartLimitAction=
. The default is a none, i.e. that no further action is taken and the service simply remains in the failure state without any further attempted restarts. The other three possible values arereboot
,reboot-force
andreboot-immediate
.reboot
attempts a clean reboot, going through the usual, clean shutdown logic.reboot-force
is more abrupt: it will not actually try to cleanly shutdown any services, but immediately kills all remaining services and unmounts all file systems and then forcibly reboots (this way all file systems will be clean but reboot will still be very fast).reboot-immediate
does not attempt to kill any process or unmount any file systems. Instead it just hard reboots the machine without delay.reboot-immediate
hence comes closest to a reboot triggered by a hardware watchdog. All these settings are documented insystemd.service(5)
.
Putting this all together we now have pretty flexible options to watchdog-supervise a specific service and configure automatic restarts of the service if it hangs, plus take ultimate action if that doesn't help.
Here's an example unit file:
[Unit] Description=My Little Daemon Documentation=man:mylittled(8) [Service] ExecStart=/usr/bin/mylittled WatchdogSec=30s Restart=on-failure StartLimitInterval=5min StartLimitBurst=4 StartLimitAction=reboot-force ```` This service will automatically be restarted if it hasn't pinged the system manager for longer than 30s or if it fails otherwise. If it is restarted this way more often than 4 times in 5min action is taken and the system quickly rebooted, with all file systems being clean when it comes up again. To write the code of the watchdog service you can follow one of these guides: - [Python based watchdog](https://sleeplessbeastie.eu/2022/08/15/how-to-create-watchdog-for-systemd-service/) - [Bash based watchdog](https://www.medo64.com/2019/01/systemd-watchdog-for-any-service/) **[Testing a watchdog](https://serverfault.com/questions/375220/how-to-check-what-if-hardware-watchdogs-are-available-in-linux)** One simple way to test a watchdog is to trigger a kernel panic. This can be done as root with: ```bash echo c > /proc/sysrq-trigger
The kernel will stop responding to the watchdog pings, so the watchdog will trigger.
SysRq is a 'magical' key combo you can hit which the kernel will respond to regardless of whatever else it is doing, unless it is completely locked up. It can also be used by echoing letters to /proc/sysrq-trigger, like we're doing here.
In this case, the letter c means perform a system crash and take a crashdump if configured.
Troubleshooting
Watchdog hardware is disabled error on boot
According to the discussion at the kernel mailing list it means that the system contains hardware watchdog but it has been disabled (probably by BIOS) and Linux cannot enable the hardware.
If your BIOS doesn't have a switch to enable it, consider the watchdog hardware broken for your system.
Some people are blacklisting the module so that it's not loaded and therefore it doesn't return the error (1, 2
References
Magic keys⚑
-
New: Introduce the Magic Keys.
The magic SysRq key is a key combination understood by the Linux kernel, which allows the user to perform various low-level commands regardless of the system's state. It is often used to recover from freezes, or to reboot a computer without corrupting the filesystem.[1] Its effect is similar to the computer's hardware reset button (or power switch) but with many more options and much more control.
This key combination provides access to powerful features for software development and disaster recovery. In this sense, it can be considered a form of escape sequence. Principal among the offered commands are means to forcibly unmount file systems, kill processes, recover keyboard state, and write unwritten data to disk. With respect to these tasks, this feature serves as a tool of last resort.
The magic SysRq key cannot work under certain conditions, such as a kernel panic[2] or a hardware failure preventing the kernel from running properly.
The key combination consists of Alt+Sys Req and another key, which controls the command issued.
On some devices, notably laptops, the Fn key may need to be pressed to use the magic SysRq key.
Reboot the machine
A common use of the magic SysRq key is to perform a safe reboot of a Linux computer which has otherwise locked up (abbr. REISUB). This can prevent a fsck being required on reboot and gives some programs a chance to save emergency backups of unsaved work. The QWERTY (or AZERTY) mnemonics: "Raising Elephants Is So Utterly Boring", "Reboot Even If System Utterly Broken" or simply the word "BUSIER" read backwards, are often used to remember the following SysRq-keys sequence:
- unRaw (take control of keyboard back from X),
- tErminate (send SIGTERM to all processes, allowing them to terminate gracefully),
- kIll (send SIGKILL to all processes, forcing them to terminate immediately),
- Sync (flush data to disk),
- Unmount (remount all filesystems read-only),
- reBoot.
When magic SysRq keys are used to kill a frozen graphical program, the program has no chance to restore text mode. This can make everything unreadable. The commands textmode (part of SVGAlib) and the reset command can restore text mode and make the console readable again.
On distributions that do not include a textmode executable, the key command Ctrl+Alt+F1 may sometimes be able to force a return to a text console. (Use F1, F2, F3,..., F(n), where n is the highest number of text consoles set up by the distribution. Ctrl+Alt+F(n+1) would normally be used to reenter GUI mode on a system on which the X server has not crashed.)
Interact with the sysrq through the commandline It can also be used by echoing letters to
/proc/sysrq-trigger
, for example to trigger a system crash and take a crashdump you can:echo c > /proc/sysrq-trigger
Monitoring⚑
Loki⚑
-
New: Prevent the too many outstanding requests error.
Add to your loki config the next options
limits_config: split_queries_by_interval: 24h max_query_parallelism: 100 query_scheduler: max_outstanding_requests_per_tenant: 4096 frontend: max_outstanding_per_tenant: 4096
-
Correction: Use
fake
when using one loki instance in docker.If you only have one Loki instance you need to save the rule yaml files in the
/etc/loki/rules/fake/
otherwise Loki will silently ignore them (it took me a lot of time to figure this out-.-
). -
New: Add alerts.
Surprisingly I haven't found any compilation of Loki alerts. I'll gather here the ones I create.
There are two kinds of rules: alerting rules and recording rules.
-
New: Alert when query returns no data.
Sometimes the queries you want to alert happen when the return value is NaN or No Data. For example if you want to monitory the happy path by setting an alert if a string is not found in some logs in a period of time.
count_over_time({filename="/var/log/mail.log"} |= `Mail is sent` [24h]) < 1
This won't trigger the alert because the
count_over_time
doesn't return a0
but aNaN
. One way to solve it is to use thevector(0)
operator with the operationor on() vector(0)
(count_over_time({filename="/var/log/mail.log"} |= `Mail is sent` [24h]) or on() vector(0)) < 1
-
Since Loki reuses the Prometheus code for recording rules and WALs, it also gains all of Prometheus’ observability.
To scrape loki metrics with prometheus add the next snippet to the prometheus configuration:
- job_name: loki metrics_path: /metrics static_configs: - targets: - loki:3100
This assumes that
loki
is a docker in the same network asprometheus
.There are some rules in the awesome prometheus alerts repo
--- groups: - name: Awesome Prometheus loki alert rules # https://samber.github.io/awesome-prometheus-alerts/rules#loki rules: - alert: LokiProcessTooManyRestarts expr: changes(process_start_time_seconds{job=~".*loki.*"}[15m]) > 2 for: 0m labels: severity: warning annotations: summary: Loki process too many restarts (instance {{ $labels.instance }}) description: "A loki process had too many restarts (target {{ $labels.instance }})\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" - alert: LokiRequestErrors expr: 100 * sum(rate(loki_request_duration_seconds_count{status_code=~"5.."}[1m])) by (namespace, job, route) / sum(rate(loki_request_duration_seconds_count[1m])) by (namespace, job, route) > 10 for: 15m labels: severity: critical annotations: summary: Loki request errors (instance {{ $labels.instance }}) description: "The {{ $labels.job }} and {{ $labels.route }} are experiencing errors\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" - alert: LokiRequestPanic expr: sum(increase(loki_panic_total[10m])) by (namespace, job) > 0 for: 5m labels: severity: critical annotations: summary: Loki request panic (instance {{ $labels.instance }}) description: "The {{ $labels.job }} is experiencing {{ printf \"%.2f\" $value }}% increase of panics\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" - alert: LokiRequestLatency expr: (histogram_quantile(0.99, sum(rate(loki_request_duration_seconds_bucket{route!~"(?i).*tail.*"}[5m])) by (le))) > 1 for: 5m labels: severity: critical annotations: summary: Loki request latency (instance {{ $labels.instance }}) description: "The {{ $labels.job }} {{ $labels.route }} is experiencing {{ printf \"%.2f\" $value }}s 99th percentile latency\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
And there are some guidelines on the rest of the metrics in the grafana documentation
Prometheus exposes a number of metrics for its WAL implementation, and these have all been prefixed with
loki_ruler_wal_
.For example:
prometheus_remote_storage_bytes_total
→loki_ruler_wal_prometheus_remote_storage_bytes_total
Additional metrics are exposed, also with the prefix
loki_ruler_wal_
. All per-tenant metrics contain a tenant label, so be aware that cardinality could begin to be a concern if the number of tenants grows sufficiently large.Some key metrics to note are:
loki_ruler_wal_appender_ready
: whether a WAL appender is ready to accept samples (1) or not (0)loki_ruler_wal_prometheus_remote_storage_samples_total
: number of samples sent per tenant to remote storageloki_ruler_wal_prometheus_remote_storage_samples_pending_total
: samples buffered in memory, waiting to be sent to remote storageloki_ruler_wal_prometheus_remote_storage_samples_failed_total
: samples that failed when sent to remote storageloki_ruler_wal_prometheus_remote_storage_samples_dropped_total
: samples dropped by relabel configurationsloki_ruler_wal_prometheus_remote_storage_samples_retried_total
: samples re-resent to remote storageloki_ruler_wal_prometheus_remote_storage_highest_timestamp_in_seconds
: highest timestamp of sample appended to WALloki_ruler_wal_prometheus_remote_storage_queue_highest_sent_timestamp_seconds
: highest timestamp of sample sent to remote storage.
-
New: Get a useful Source link in the alertmanager.
This still doesn't work. Currently for the ruler
external_url
if you use the URL of your Grafana installation: e.g.external_url: "https://grafana.example.com"
it creates a Source link in alertmanager similar to https://grafana.example.com/graph?g0.expr=%28sum+by%28thing%29%28count_over_time%28%7Bnamespace%3D%22foo%22%7D+%7C+json+%7C+bar%3D%22maxRetries%22%5B5m%5D%29%29+%3E+0%29&g0.tab=1, which isn't valid.This url templating (via
/graph?g0.expr=%s&g0.tab=1
) appears to be coming from prometheus. There is not a workaround yet -
New: Maximum of series reached for a single query.
Go to the loki-local-config.yaml, then find the limits_config configuration. Then modify this to the limits_config:
limits_config: max_query_series: 100000
But probably you're doing something wrong. feat(orgzly#Both local and remote notebook have been modified): Both local and remote notebook have been modified
You can force load or force save a single note with a long tap.
-
Correction: Don't use vector(0) on aggregation over labels.
If you're doing an aggregation over a label this approach won't work because it will add a new time series with value 0. In those cases use a broader search that includes other logs from the label you're trying to aggregate and multiply it by 0. For example:
( sum by (hostname) ( count_over_time({job="systemd-journal", syslog_identifier="sanoid"}[1h]) ) or sum by (hostname) ( count_over_time({job="systemd-journal"}[1h]) * 0 ) ) < 1
The first part of the query returns all log lines of the service
sanoid
for eachhostname
. If one hostname were not to return any line that query alone won't show anything for that host. The second part of the query counts all the log lines of eachhostname
, so if it's up it will probably be sending at least one line per hour. As we're not interested in those number of lines we multiply it by 0, so that the target is shown. -
New: Interact with loki through python.
There is no client library for python (1, 2) they suggest to interact with the API with
requests
. Although I'd rather uselogcli
with thesh
library. -
New: Download the logs.
The web UI only allows you to download the logs that are loaded in the view, if you want to download big amounts of logs you need to either use
logcli
or interact with the API.One user did a query on loop:
set -x JOB_ID=9079dc54-2f5c-4d74-a9aa-1d9eb39dd3c2 for I in `seq 0 655`; do FILE=logs_$I.txt ID="$JOB_ID:$I" QUERY="{aws_job_id=\"$ID\",job=\"varlogs\"}" docker run grafana/logcli:main-1b6d0bf-amd64 --addr=http://localhost:3100/ -o raw -q query $QUERY --limit 100000 --batch 100 --forward --from "2022-09-25T10:00:00Z" > $FILE done
Logcli⚑
-
New: Introduce Promtail.
Promtail is an agent which ships the contents of local logs to a Loki instance.
It is usually deployed to every machine that runs applications which need to be monitored.
It primarily:
- Discovers targets
- Attaches labels to log streams
- Pushes them to the Loki instance.
-
On systems with
systemd
, Promtail also supports reading from the journal. Unlike file scraping which is defined in thestatic_configs
stanza, journal scraping is defined in ajournal
stanza:scrape_configs: - job_name: journal journal: json: false max_age: 12h path: /var/log/journal labels: job: systemd-journal relabel_configs: - source_labels: ['__journal__systemd_unit'] target_label: unit - source_labels: ['__journal__hostname'] target_label: hostname - source_labels: ['__journal_syslog_identifier'] target_label: syslog_identifier - source_labels: ['__journal_transport'] target_label: transport - source_labels: ['__journal_priority_keyword'] target_label: keyword
All fields defined in the journal section are optional, and are just provided here for reference.
max_age
ensures that no older entry than the time specified will be sent to Loki; this circumventsentry too old
errors.path
tells Promtail where to read journal entries from.labels
map defines a constant list of labels to add to every journal entry that Promtail reads.matches
field adds journal filters. If multiple filters are specified matching different fields, the log entries are filtered by both, if two filters apply to the same field, then they are automatically matched as alternatives.- When the
json
field is set to true, messages from the journal will be passed through the pipeline as JSON, keeping all of the original fields from the journal entry. This is useful when you don’t want to index some fields but you still want to know what values they contained. - When Promtail reads from the journal, it brings in all fields prefixed with
__journal_
as internal labels. Like in the example above, the_SYSTEMD_UNIT
field from the journal was transformed into a label calledunit
throughrelabel_configs
. Keep in mind that labels prefixed with__
will be dropped, so relabeling is required to keep these labels. Look at the systemd man pages for a list of fields exposed by the journal.
By default, Promtail reads from the journal by looking in the
/var/log/journal
and/run/log/journal
paths. If running Promtail inside of a Docker container, the path appropriate to your distribution should be bind mounted inside of Promtail along with binding/etc/machine-id
. Bind mounting/etc/machine-id
to the path of the same name is required for the journal reader to know which specific journal to read from.docker run \ -v /var/log/journal/:/var/log/journal/ \ -v /run/log/journal/:/run/log/journal/ \ -v /etc/machine-id:/etc/machine-id \ grafana/promtail:latest \ -config.file=/path/to/config/file.yaml
-
New: Scrape docker logs.
Docker service discovery allows retrieving targets from a Docker daemon. It will only watch containers of the Docker daemon referenced with the host parameter. Docker service discovery should run on each node in a distributed setup. The containers must run with either the
json-file
orjournald
logging driver.Note that the discovery will not pick up finished containers. That means Promtail will not scrape the remaining logs from finished containers after a restart.
The available meta labels are:scrape_configs: - job_name: docker docker_sd_configs: - host: unix:///var/run/docker.sock refresh_interval: 5s relabel_configs: - source_labels: ['__meta_docker_container_id'] target_label: docker_id - source_labels: ['__meta_docker_container_name'] target_label: docker_name
__meta_docker_container_id
: the ID of the container__meta_docker_container_name
: the name of the container__meta_docker_container_network_mode
: the network mode of the container__meta_docker_container_label_<labelname>
: each label of the container__meta_docker_container_log_stream
: the log stream type stdout or stderr__meta_docker_network_id
: the ID of the network__meta_docker_network_name
: the name of the network__meta_docker_network_ingress
: whether the network is ingress__meta_docker_network_internal
: whether the network is internal__meta_docker_network_label_<labelname>
: each label of the network__meta_docker_network_scope
: the scope of the network__meta_docker_network_ip
: the IP of the container in this network__meta_docker_port_private
: the port on the container__meta_docker_port_public
: the external port if a port-mapping exists__meta_docker_port_public_ip
: the public IP if a port-mapping exists
These labels can be used during relabeling. For instance, the following configuration scrapes the container named
flog
and removes the leading slash (/) from the container name. yamlscrape_configs: - job_name: flog_scrape docker_sd_configs: - host: unix:///var/run/docker.sock refresh_interval: 5s filters: - name: name values: [flog] relabel_configs: - source_labels: ['__meta_docker_container_name'] regex: '/(.*)' target_label: 'container'
-
New: Pipeline building.
In this issue there are nice examples on different pipelines.
-
New: Drop logs.
If you don't want the logs that have the keyword
systemd-journal
and valuedocker-compose
you can add the next pipeline stage:pipeline_stages: - drop: source: syslog_identifier value: docker-compose
-
Use patrickjahns ansible role. Some interesting variables are:
loki_url: localhost promtail_system_user: root promtail_config_clients: - url: "http://{{ loki_url }}:3100/loki/api/v1/push" external_labels: hostname: "{{ ansible_hostname }}"
-
New: Troubleshooting promtail.
Find where is the
positions.yaml
file and see if it evolves.Sometimes if you are not seeing the logs in loki it's because the query you're running is not correct.
-
New: Set the hostname label on all logs.
There are many ways to do it:
- Setting the label in the promtail launch command
bash sudo ./promtail-linux-amd64 --client.url=http://xxxx:3100/loki/api/v1/push --client.external-labels=hostname=$(hostname) --config.file=./config.yaml
This won't work if you're using promtail within a docker-compose because you can't use bash expansion in the
docker-compose.yaml
file - Allowing env expansion and setting it in the promtail conf. You can launch the promtail command with-config.expand-env
and then set in each scrape jobs:This won't work either if you're usinglabels: host: ${HOSTNAME}
promtail
within a docker as it will give you the ID of the docker - Set it in thepromtail_config_clients
field asexternal_labels
of each promtail config:- Hardcode it for each promtail config scraping config as static labels. If you're using ansible or any deployment method that supports jinja expansion set it that waypromtail_config_clients: - url: "http://{{ loki_url }}:3100/loki/api/v1/push" external_labels: hostname: "{{ ansible_hostname }}"
labels: host: {{ ansible_hostname }}
- Setting the label in the promtail launch command
-
New: Introduce logcli.
logcli
is the command-line interface to Grafana Loki. It facilitates running LogQL queries against a Loki instance.Installation Download the logcli binary from the Loki releases page and install it somewhere in your
$PATH
.Usage
logcli
points to the local instancehttp://localhost:3100
directly, if you want another one export theLOKI_ADDR
environment variable.Run a query:
logcli query '{job="loki-ops/consul"}'
You can also set the time range and output format
logcli query \ --timezone=UTC \ --from="2024-06-10T07:23:36Z" \ --to="2024-06-12T16:23:58Z" \ --output=jsonl \ '{job="docker", container="aleph_ingest-file_1"} | json | __error__=`` | severity =~ `WARNING|ERROR` | message !~ `Queueing failed task for retry.*` | logger!=`ingestors.manager`'
References
Grafana⚑
-
New: Copy panels between dashboards.
On each panel on the top right you can select
copy
, then on the menu to add a new panel you can click onPaste panel from clipboard
.So far you can't do this for rows.
AlertManager⚑
-
Correction: Add another source on how to silence alerts.
If previous guidelines don't work for you, you can use the sleep peacefully guidelines to tackle it at query level.
-
New: Using time intervals.
The values of
time_intervals
can be:- times: [ - <time_range> ...] weekdays: [ - <weekday_range> ...] days_of_month: [ - <days_of_month_range> ...] months: [ - <month_range> ...] years: [ - <year_range> ...] location: <string>
All fields are lists. Within each non-empty list, at least one element must be satisfied to match the field. If a field is left unspecified, any value will match the field. For an instant of time to match a complete time interval, all fields must match. Some fields support ranges and negative indices, and are detailed below. If a time zone is not specified, then the times are taken to be in UTC.
-
time_range
: Ranges inclusive of the starting time and exclusive of the end time to make it easy to represent times that start/end on hour boundaries. For example,start_time: '17:00'
andend_time: '24:00'
will begin at 17:00 and finish immediately before 24:00. They are specified like so:times: - start_time: HH:MM end_time: HH:MM
-
weekday_range
: A list of days of the week, where the week begins on Sunday and ends on Saturday. Days should be specified by name (e.g. 'Sunday'). For convenience, ranges are also accepted of the form<start_day>:<end_day>
and are inclusive on both ends. For example:['monday:wednesday','saturday', 'sunday']
-
days_of_month_range
: A list of numerical days in the month. Days begin at1
. Negative values are also accepted which begin at the end of the month, e.g.-1
during January would represent January 31. For example:['1:5', '-3:-1']
. Extending past the start or end of the month will cause it to be clamped. E.g. specifying['1:31']
during February will clamp the actual end date to 28 or 29 depending on leap years. Inclusive on both ends. -
month_range
: A list of calendar months identified by a case-insensitive name (e.g. 'January') or by number, whereJanuary = 1
. Ranges are also accepted. For example,['1:3', 'may:august', 'december']
. Inclusive on both ends. -
year_range
: A numerical list of years. Ranges are accepted. For example,['2020:2022', '2030']
. Inclusive on both ends. -
location
: A string that matches a location in the IANA time zone database. For example,'Australia/Sydney'
. The location provides the time zone for the time interval. For example, a time interval with a location of'Australia/Sydney'
that contained something like:
times: - start_time: 09:00 end_time: 17:00 weekdays: ['monday:friday']
Would include any time that fell between the hours of 9:00AM and 5:00PM, between Monday and Friday, using the local time in Sydney, Australia. You may also use
'Local'
as a location to use the local time of the machine where Alertmanager is running, or'UTC'
for UTC time. If no timezone is provided, the time interval is taken to be in UTC. -
Process Exporter⚑
-
New: Introduce the process exporter.
process_exporter
is a rometheus exporter that mines /proc to report on selected processes.References - Source - Grafana dashboard
Databases⚑
PostgreSQL⚑
-
New: Store expensive calculation values in a postgresql database.
First you need to think if you actually need to store the calculations or you can do them on the fly with views. If views are too slow you can either use materialized views or triggers over calculation tables.
Materialized views are simpler to maintain but have some disadvantages such as outdated data or unneeded processing of data. If you need totally current information or if you don't want to periodically do the calculations on all the rows then triggers are probably the better solution.
-
New: Drop all tables of a database.
drop schema public cascade; create schema public;
-
New: Views.
A view is a named query stored in the PostgreSQL database server. A view is defined based on one or more tables which are known as base tables, and the query that defines the view is referred to as a defining query.
After creating a view, you can query data from it as you would from a regular table. Behind the scenes, PostgreSQL will rewrite the query against the view and its defining query, executing it to retrieve data from the base tables.
Views do not store data except the materialized views. In PostgreSQL, you can create special views called materialized views that store data physically and periodically refresh it from the base tables.
Simple views can be updatable.
Advantages of views - Simplifying complex queries: Views help simplify complex queries. Instead of dealing with joins, aggregations, or filtering conditions, you can query from views as if they were regular tables.
Typically, first, you create views based on complex queries and store them in the database. Then, you can use simple queries based on views instead of using complex queries.
-
Logical data independence: If your applications use views, you can freely modify the structure of the base tables. In other words, views enable you to create a layer of abstraction over the underlying tables.
-
Security and access control: Views enable fine-grained control over data access. You can create views that expose subsets of data in the base tables, hiding sensitive information.
This is particularly useful when you have applications that require access to distinct portions of the data.
Creating a view In PostgreSQL, a view is a named query stored in the database server. To create a new view, you can use the
CREATE VIEW
statement.CREATE VIEW view_name AS query;
In this syntax:
- Specify the name of the view after the
CREATE VIEW
keywords. - Specify a
SELECT
statement (query) that defines the view. The query is often referred to as the defining query of the view.
Creating a view examples
We’ll use the customer table from the sample database:
Basic CREATE VIEW statement example
The following example uses the CREATE VIEW statement to create a view based on the customer table:
CREATE VIEW contact AS SELECT first_name, last_name, email FROM customer;
Output:
CREATE VIEW
The following query data from the contact view:
SELECT * FROM contact;
Output:
first_name | last_name | email -------------+--------------+------------------------------------------ Jared | Ely | jared.ely@sakilacustomer.org Mary | Smith | mary.smith@sakilacustomer.org Patricia | Johnson | patricia.johnson@sakilacustomer.org ...
Using the CREATE VIEW statement to create a view based on a complex query
The following example creates a view based on the tables customer, address, city, and country:
CREATE VIEW customer_info AS SELECT first_name, last_name, email, phone, city, postal_code, country FROM customer INNER JOIN address USING (address_id) INNER JOIN city USING (city_id) INNER JOIN country USING (country_id);
The following query retrieves data from the
customer_info
view:SELECT * FROM customer_info;
Output:
first_name | last_name | email | phone | city | postal_code | country -------------+--------------+------------------------------------------+--------------+----------------------------+-------------+--------------------------------------- Jared | Ely | jared.ely@sakilacustomer.org | 35533115997 | Purwakarta | 25972 | Indonesia Mary | Smith | mary.smith@sakilacustomer.org | 28303384290 | Sasebo | 35200 | Japan Patricia | Johnson | patricia.johnson@sakilacustomer.org | 838635286649 | San Bernardino | 17886 | United States ...
Creating a view based on another view
The following statement creates a view called
customer_usa
based on thecustomer_info
view. Thecustomer_usa
returns the customers who are in the United States:CREATE VIEW customer_usa AS SELECT * FROM customer_info WHERE country = 'United States';
Here’s the query that retrieves data from the customer_usa view:
SELECT * FROM customer_usa;
Output:
first_name | last_name | email | phone | city | postal_code | country ------------+------------+--------------------------------------+--------------+-------------------------+-------------+--------------- Zachary | Hite | zachary.hite@sakilacustomer.org | 191958435142 | Akron | 88749 | United States Richard | Mccrary | richard.mccrary@sakilacustomer.org | 262088367001 | Arlington | 42141 | United States Diana | Alexander | diana.alexander@sakilacustomer.org | 6171054059 | Augusta-Richmond County | 30695 | United States ...
Replacing a view Note: for simple changes check alter views
To change the defining query of a view, you use the
CREATE OR REPLACE VIEW
statement:CREATE OR REPLACE VIEW view_name AS query;
In this syntax, you add the
OR REPLACE
between theCREATE
andVIEW
keywords. If the view already exists, the statement replaces the existing view; otherwise, it creates a new view.For example, the following statement changes the defining query of the contact view to include the phone information from the address table:
CREATE OR REPLACE VIEW contact AS SELECT first_name, last_name, email, phone FROM customer INNER JOIN address USING (address_id);
Display a view on psql
To display a view on
psql
, you follow these steps:First, open the Command Prompt on Windows or Terminal on Unix-like systems and connect to the PostgreSQL server:
psql -U postgres
Second, change the current database to
dvdrental
:\c dvdrental
Third, display the view information using the
\d+ view_name
command. For example, the following shows the contact view:\d+ contact
Output:
View "public.contact" Column | Type | Collation | Nullable | Default | Storage | Description ------------+-----------------------+-----------+----------+---------+----------+------------- first_name | character varying(45) | | | | extended | last_name | character varying(45) | | | | extended | email | character varying(50) | | | | extended | phone | character varying(20) | | | | extended | View definition: SELECT customer.first_name, customer.last_name, customer.email, address.phone FROM customer JOIN address USING (address_id);
-
-
New: Materialized Views.
PostgreSQL extends the view concept to the next level which allows views to store data physically. These views are called materialized views.
Materialized views cache the result set of an expensive query and allow you to refresh data periodically.
The materialized views can be useful in many cases that require fast data access. Therefore, you often find them in data warehouses and business intelligence applications.
Benefits of materialized views
- Improve query efficiency: If a query takes a long time to run, it could be because there are a lot of transformations being done to the data: subqueries, functions, and joins, for example.
A materialized view can combine all of that into a single result set that’s stored like a table.
This means that any user or application that needs to get this data can just query the materialized view itself, as though all of the data is in the one table, rather than running the expensive query that uses joins, functions, or subqueries.
Calculations can also be added to materialized views for any fields you may need, which can save time, and are often not stored in the database.
- Simplify a query: Like a regular view, a materialized view can also be used to simplify a query. If a query is using a lot of logic such as joins and functions, using a materialized view can help remove some of that logic and place it into the materialized view.
Disadvantages of a Materialized View - Updates to data need to be set up: The main disadvantage to using materialized views is that the data needs to be refreshed.
The data that’s used to populate the materialized view is stored in the database tables. These tables can have their data updated, inserted, or deleted. When that happens, the data in the materialized view needs to be updated.
This can be done manually, but it should be done automatically.
- Incremental updates are not supported: So the whole view is generated on each refresh.
- Data may be inconsistent: Because the data is stored separately in the materialized view, the data in the materialized view may be inconsistent with the data in the underlying tables.
This may be an issue if you are expecting or relying on data to be consistent.
However, for scenarios where it doesn’t matter (e.g. monthly reporting on months in the past), then it may be OK.
- Storage Requirements: Materialized Views can consume significant storage space, depending on the size of your dataset. This consideration is crucial, especially in resource-limited environments.
Creating materialized views
To create a materialized view, you use the CREATE MATERIALIZED VIEW statement as follows:
CREATE MATERIALIZED VIEW [IF NOT EXISTS] view_name AS query WITH [NO] DATA;
How it works.
- First, specify the
view_name
after theCREATE MATERIALIZED VIEW
clause - Second, add the
query
that retrieves data from the underlying tables after theAS
keyword. - Third, if you want to load data into the materialized view at the creation time, use the
WITH DATA
option; otherwise, you useWITH NO DATA
option. If you use theWITH NO DATA
option, the view is flagged as unreadable. It means that you cannot query data from the view until you load data into it. - Finally, use the
IF NOT EXISTS
option to conditionally create a view only if it does not exist.
Refreshing data for materialized views
Postgresql will never refresh the data by it's own, you need to define the processes that will update it.
To load or update the data into a materialized view, you use the
REFRESH MATERIALIZED VIEW
statement:REFRESH MATERIALIZED VIEW view_name;
When you refresh data for a materialized view, PostgreSQL locks the underlying tables. Consequently, you will not be able to retrieve data from underlying tables while data is loading into the view.
To avoid this, you can use the
CONCURRENTLY
option.REFRESH MATERIALIZED VIEW CONCURRENTLY view_name;
With the
CONCURRENTLY
option, PostgreSQL creates a temporary updated version of the materialized view, compares two versions, and performsINSERT
andUPDATE
only the differences.PostgreSQL allows you to retrieve data from a materialized view while it is being updated. One requirement for using
CONCURRENTLY
option is that the materialized view must have aUNIQUE
index.Automatic update of materialized views
Removing materialized views
To remove a materialized view, you use the
DROP MATERIALIZED VIEW
statement:DROP MATERIALIZED VIEW view_name;
In this syntax, you specify the name of the materialized view that you want to drop after the
DROP MATERIALIZED VIEW
keywords.Materialized view example We’ll use the tables in the sample database for creating a materialized view.
First, create a materialized view named
rental_by_category
using theCREATE MATERIALIZED VIEW
statement:CREATE MATERIALIZED VIEW rental_by_category AS SELECT c.name AS category, sum(p.amount) AS total_sales FROM (((((payment p JOIN rental r ON ((p.rental_id = r.rental_id))) JOIN inventory i ON ((r.inventory_id = i.inventory_id))) JOIN film f ON ((i.film_id = f.film_id))) JOIN film_category fc ON ((f.film_id = fc.film_id))) JOIN category c ON ((fc.category_id = c.category_id))) GROUP BY c.name ORDER BY sum(p.amount) DESC WITH NO DATA;
Because of the
WITH NO DATA
option, you cannot query data from the view. If you attempt to do so, you’ll get the following error message:SELECT * FROM rental_by_category;
Output:
[Err] ERROR: materialized view "rental_by_category" has not been populated HINT: Use the REFRESH MATERIALIZED VIEW command.
PostgreSQL is helpful to give you a hint to ask for loading data into the view.
Second, load data into the materialized view using the
REFRESH MATERIALIZED VIEW
statement:REFRESH MATERIALIZED VIEW rental_by_category;
Third, retrieve data from the materialized view:
SELECT * FROM rental_by_category;
Output:
category | total_sales -------------+------------- Sports | 4892.19 Sci-Fi | 4336.01 Animation | 4245.31 Drama | 4118.46 Comedy | 4002.48 New | 3966.38 Action | 3951.84 Foreign | 3934.47 Games | 3922.18 Family | 3830.15 Documentary | 3749.65 Horror | 3401.27 Classics | 3353.38 Children | 3309.39 Travel | 3227.36 Music | 3071.52 (16 rows)
From now on, you can refresh the data in the
rental_by_category
view using theREFRESH MATERIALIZED VIEW
statement.However, to refresh it with
CONCURRENTLY
option, you need to create aUNIQUE
index for the view first.CREATE UNIQUE INDEX rental_category ON rental_by_category (category);
Let’s refresh data concurrently for the
rental_by_category
view.REFRESH MATERIALIZED VIEW CONCURRENTLY rental_by_category;
Hardware⚑
ECC RAM⚑
-
New: Introduce ECC RAM.
Error Correction Code (ECC) is a mechanism used to detect and correct errors in memory data due to environmental interference and physical defects. ECC memory is used in high-reliability applications that cannot tolerate failure due to corrupted data.
Installation: Due to additional circuitry required for ECC protection, specialized ECC hardware support is required by the CPU chipset, motherboard and DRAM module. This includes the following:
- Server-grade CPU chipset with ECC support (Intel Xeon, AMD Ryzen)
- Motherboard supporting ECC operation
- ECC RAM
Consult the motherboard and/or CPU documentation for the specific model to verify whether the hardware supports ECC. Use vendor-supplied list of certified ECC RAM, if provided.
Most ECC-supported motherboards allow you to configure ECC settings from the BIOS setup. They are usually on the Advanced tab. The specific option depends on the motherboard vendor or model such as the following:
- DRAM ECC Enable (American Megatrends, ASUS, ASRock, MSI)
- ECC Mode (ASUS)
Monitorization
The mechanism for how ECC errors are logged and reported to the end-user depends on the BIOS and operating system. In most cases, corrected ECC errors are written to system/event logs. Uncorrected ECC errors may result in kernel panic or blue screen.
The Linux kernel supports reporting ECC errors for ECC memory via the EDAC (Error Detection And Correction) driver subsystem. Depending on the Linux distribution, ECC errors may be reported by the following:
rasdaemon
: monitor ECC memory and report both correctable and uncorrectable memory errors on recent Linux kernels.mcelog
(Deprecated): collects and decodes MCA error events on x86.edac-utils
(Deprecated): fills DIMM labels data and summarizes memory errors.
To configure rasdaemon follow this article
Confusion on boards supporting ECC
I've read that even if some motherboards say that they "Support ECC" some of them don't do anything with it.
On this post and the kernel docs show that you should see references to ACPI/WHEA in the specs manual. Ideally ACPI5 support.
From the ) EINJ provides a hardware error injection mechanism. It is very useful for debugging and testing APEI and RAS features in general.
You need to check whether your BIOS supports EINJ first. For that, look for early boot messages similar to this one:
ACPI: EINJ 0x000000007370A000 000150 (v01 INTEL 00000001 INTL 00000001)
Which shows that the BIOS is exposing an EINJ table - it is the mechanism through which the injection is done.
Alternatively, look in
/sys/firmware/acpi/tables
for an "EINJ" file, which is a different representation of the same thing.It doesn't necessarily mean that EINJ is not supported if those above don't exist: before you give up, go into BIOS setup to see if the BIOS has an option to enable error injection. Look for something called
WHEA
or similar. Often, you need to enable anACPI5
support option prior, in order to see theAPEI
,EINJ
,... functionality supported and exposed by the BIOS menu.To use
EINJ
, make sure the following are options enabled in your kernel configuration:CONFIG_DEBUG_FS CONFIG_ACPI_APEI CONFIG_ACPI_APEI_EINJ
One way to test it can be to run memtest as it sometimes shows ECC errors such as
** Warning** ECC injection may be disabled for AMD Ryzen (70h-7fh)
.Other people (1, 2 say that there are a lot of motherboards that NEVER report any corrected errors to the OS. In order to see corrected errors, PFEH (Platform First Error Handling) has to be disabled. On some motherboards and FW versions this setting is hidden from the user and always enabled, thus resulting in zero correctable errors getting reported.
They also suggest to disable "Quick Boot". In order to initialize ECC, memory has to be written before it can be used. Usually this is done by BIOS, but with some motherboards this step is skipped if "Quick Boot" is enabled.
The people behind memtest have a paid tool to test ECC
-
New: Introduce rasdaemon the ECC monitor.
rasdaemon
is a RAS (Reliability, Availability and Serviceability) logging tool. It records memory errors, using the EDAC tracing events. EDAC is a Linux kernel subsystem with handles detection of ECC errors from memory controllers for most chipsets on i386 and x86_64 architectures. EDAC drivers for other architectures like arm also exists.Installation
apt-get install rasdaemon
The output will be available via syslog but you can show it to the foreground (
-f
) or to an sqlite3 database (-r
)To post-process and decode received MCA errors on AMD SMCA systems, run:
rasdaemon -p --status <STATUS_reg> --ipid <IPID_reg> --smca --family <CPU Family> --model <CPU Model> --bank <BANK_NUM>
Status and IPID Register values (in hex) are mandatory. The smca flag with family and model are required if not decoding locally. Bank parameter is optional.
You may also start it via systemd:
systemctl start rasdaemon
The rasdaemon will then output the messages to journald.
At this point
rasdaemon
should already be running on your system. You can now use theras-mc-ctl
tool to query the errors that have been detected. If everything is well configured you'll see something like:$: ras-mc-ctl --error-count Label CE UE mc#0csrow#2channel#0 0 0 mc#0csrow#2channel#1 0 0 mc#0csrow#3channel#1 0 0 mc#0csrow#3channel#0 0 0
If it's not you'll see:
ras-mc-ctl: Error: No DIMMs found in /sys or new sysfs EDAC interface not found.
The
CE
column represents the number of corrected errors for a given DIMM,UE
represents uncorrectable errors that were detected. The label on the left shows the EDAC path under/sys/devices/system/edac/mc/
of every DIMM. This is not very readable, if you wish to improve the labeling read this articleMore ways to check is to run:
$: ras-mc-ctl --status ras-mc-ctl: drivers are loaded.
You can also see a summary of the state with:
$: ras-mc-ctl --summary No Memory errors. No PCIe AER errors. No Extlog errors. DBD::SQLite::db prepare failed: no such table: devlink_event at /usr/sbin/ras-mc-ctl line 1183. Can't call method "execute" on an undefined value at /usr/sbin/ras-mc-ctl line 1184.
Monitorization
You can use loki to monitor ECC errors shown in the logs with the next alerts:
Referencesgroups: - name: ecc rules: - alert: ECCError expr: | count_over_time({job="systemd-journal", unit="rasdaemon.service", level="error"} [5m]) > 0 for: 1m labels: severity: critical annotations: summary: "Possible ECC error detected in {{ $labels.hostname}}" - alert: ECCWarning expr: | count_over_time({job="systemd-journal", unit="rasdaemon.service", level="warning"} [5m]) > 0 for: 1m labels: severity: warning annotations: summary: "Possible ECC warning detected in {{ $labels.hostname}}" - alert: ECCAlert expr: | count_over_time({job="systemd-journal", unit="rasdaemon.service", level!~"info|error|warning"} [5m]) > 0 for: 1m labels: severity: info annotations: summary: "ECC log trace with unknown severity level detected in {{ $labels.hostname}}"
-
New: Check if system is actually using ECC.
Another way is to run
dmidecode
. For ECC support you'll see:$: dmidecode -t memory | grep ECC Error Correction Type: Single-bit ECC # or Error Correction Type: Multi-bit ECC
No ECC:
$: dmidecode -t memory | grep ECC Error Correction Type: None
You can also test it with
rasdaemon
GPU⚑
-
New: Introduce GPU.
GPU or Graphic Processing Unit is a specialized electronic circuit initially designed to accelerate computer graphics and image processing (either on a video card or embedded on motherboards, mobile phones, personal computers, workstations, and game consoles).
For years I've wanted to buy a graphic card but I've been stuck in the problem that I don't have a desktop. I have a X280 lenovo laptop used to work and personal use with an integrated card that has let me so far to play old games such as King Arthur Gold or Age of Empires II, but has hard times playing "newer" games such as It takes two. Last year I also bought a NAS with awesome hardware. So it makes no sense to buy a desktop just for playing.
Now that I host Jellyfin on the NAS and that machine learning is on the hype with a lot of interesting solutions that can be self-hosted (whisper, chatgpt similar solutions...), it starts to make sense to add a GPU to the server. What made me give the step is that you can also self-host a gaming server to stream to any device! It makes so much sense to have all the big guns inside the NAS and stream the content to the less powerful devices.
That way if you host services, you make the most use of the hardware.
-
New: Install cuda.
CUDA is a parallel computing platform and programming model invented by NVIDIA®. It enables dramatic increases in computing performance by harnessing the power of the graphics processing unit (GPU). If you're not using Debian 11 follow these instructions
Base Installer
wget https://developer.download.nvidia.com/compute/cuda/12.5.1/local_installers/cuda-repo-debian11-12-5-local_12.5.1-555.42.06-1_amd64.deb sudo dpkg -i cuda-repo-debian11-12-5-local_12.5.1-555.42.06-1_amd64.deb sudo cp /var/cuda-repo-debian11-12-5-local/cuda-*-keyring.gpg /usr/share/keyrings/ sudo add-apt-repository contrib sudo apt-get update sudo apt-get -y install cuda-toolkit-12-5
Additional installation options are detailed here.
Driver Installer
To install the open kernel module flavor:
sudo apt-get install -y nvidia-kernel-open-dkms sudo apt-get install -y cuda-drivers
Install cuda:
apt-get install cuda reboot
Install nvidia card
Check if your card is supported in the releases supported by your OS - If it's supported - If it's not supported
Ensure the GPUs are Installed
Install
pciutils
:Ensure that the
lspci
command is installed (which lists the PCI devices connected to the server):sudo apt-get -y install pciutils
Check Installed Nvidia Cards: Perform a quick check to determine what Nvidia cards have been installed:
lspci | grep VGA
The output of the
lspci
command above should be something similar to:00:02.0 VGA compatible controller: Intel Corporation 4th Gen ... 01:00.0 VGA compatible controller: Nvidia Corporation ...
If you do not see a line that includes Nvidia, then the GPU is not properly installed. Otherwise, you should see the make and model of the GPU devices that are installed.
Disable Nouveau
Blacklist Nouveau in Modprobe: The
nouveau
driver is an alternative to the Nvidia drivers generally installed on the server. It does not work with CUDA and must be disabled. The first step is to edit the file at/etc/modprobe.d/blacklist-nouveau.conf
.Create the file with the following content:
cat <<EOF | sudo tee /etc/modprobe.d/blacklist-nouveau.conf blacklist nouveau blacklist lbm-nouveau options nouveau modeset=0 alias nouveau off alias lbm-nouveau off EOF
Then, run the following commands:
echo options nouveau modeset=0 | sudo tee -a /etc/modprobe.d/nouveau-kms.conf sudo update-initramfs -u
Update Grub to Blacklist Nouveau:
Backup your grub config template:
sudo cp /etc/default/grub /etc/default/grub.bak
Then, update your grub config template at
/etc/default/grub
. Addrd.driver.blacklist=nouveau
andrcutree.rcu_idle_gp_delay=1
to theGRUB_CMDLINE_LINUX
variable. For example, change:GRUB_CMDLINE_LINUX="quiet"
to:
GRUB_CMDLINE_LINUX="quiet rd.driver.blacklist=nouveau rcutree.rcu_idle_gp_delay=1"
Then, rebuild your grub config:
sudo grub2-mkconfig -o /boot/grub/grub.cfg
Install prerequisites
The following prerequisites should be installed before installing the Nvidia drivers:
sudo apt-get -y install linux-headers-$(uname -r) make gcc-4.8 sudo apt-get -y install acpid dkms
Close X Server:
Before running the install, you should exit out of any X environment, such as Gnome, KDE, or XFCE. To exit the X session, switch to a TTY console using
Ctrl-Alt-F1
and then determine whether you are runninglightdm
orgdm
by running:sudo ps aux | grep "lightdm|gdm|kdm"
Depending on which is running, stop the service, running the following commands (substitute
gdm
orkdm
forlightdm
as appropriate):sudo service lightdm stop sudo init 3
Install Drivers Only:
To accommodate GL-accelerated rendering, OpenGL and GL Vendor Neutral Dispatch (GLVND) are now required and should be installed with the Nvidia drivers. OpenGL is an installation option in the
*.run
type of drivers. In other types of the drivers, OpenGL is enabled by default in most modern versions (dated 2016 and later). GLVND can be installed using the installer menus or via the--glvnd-glx-client
command line flag.This section deals with installing the drivers via the
*.run
executables provided by Nvidia.To download only the drivers, navigate to http://www.nvidia.com/object/unix.html and click the Latest Long Lived Branch version under the appropriate CPU architecture. On the ensuing page, click Download and then click Agree and Download on the page that follows.
The Unix drivers found in the link above are also compatible with all Nvidia Tesla models.
If you'd prefer to download the full driver repository, Nvidia provides a tool to recommend the most recent available driver for your graphics card at http://www.Nvidia.com/Download/index.aspx?lang=en-us.
If you are unsure which Nvidia devices are installed, the
lspci
command should give you that information:lspci | grep -i "nvidia"
Download the recommended driver executable. Change the file permissions to allow execution:
chmod +x ./NVIDIA-Linux-$(uname -m)-*.run
Run the install.
To check that the GPU is well installed and functioning properly, you can use the
nvidia-smi
command. This command provides detailed information about the installed Nvidia GPUs, including their status, utilization, and driver version.First, ensure the Nvidia drivers are installed. Then, run:
nvidia-smi
If the GPU is properly installed, you should see an output that includes information about the GPU, such as its model, memory usage, and driver version. The output will look something like this:
+-----------------------------------------------------------------------------+ | NVIDIA-SMI 450.66 Driver Version: 450.66 CUDA Version: 11.0 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |===============================+======================+======================| | 0 Tesla K80 Off | 00000000:00:1E.0 Off | 0 | | N/A 38C P8 29W / 149W | 0MiB / 11441MiB | 0% Default | | | | N/A | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=============================================================================| | No running processes found | +-----------------------------------------------------------------------------+
If you encounter any errors or the GPU is not listed, there may be an issue with the installation or configuration of the GPU drivers.
For Nvidia GPUs there is a tool nvidia-smi that can show memory usage, GPU utilization and temperature of GPU.
First make sure you have CUDA installed, then install the
gpu_burn
toolgit clone https://github.com/wilicc/gpu-burn cd gpu-burn make
To run a test for 60 seconds run:
./gpu_burn 60
NVIDIA DCGM is a set of tools for managing and monitoring NVIDIA GPUs in large-scale, Linux-based cluster environments. It’s a low overhead tool that can perform a variety of functions including active health monitoring, diagnostics, system validation, policies, power and clock management, group configuration, and accounting. For more information, see the DCGM User Guide.
You can use DCGM to expose GPU metrics to Prometheus using
dcgm-exporter
.- Install NVIDIA Container Kit: The NVIDIA Container Toolkit allows users to build and run GPU accelerated containers. The toolkit includes a container runtime library and utilities to automatically configure containers to leverage NVIDIA GPUs.
sudo apt-get install -y nvidia-container-toolkit
- Configure the container runtime by using the nvidia-ctk command:
- Restart the Docker daemon:sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
- Install NVIDIA DCGM: Follow the Getting Started Guide.
Determine the distribution name:
distribution=$(. /etc/os-release;echo $ID$VERSION_ID | sed -e 's/\.//g')
Download the meta-package to set up the CUDA network repository:
wget https://developer.download.nvidia.com/compute/cuda/repos/$distribution/x86_64/cuda-keyring_1.1-1_all.deb
Install the repository meta-data and the CUDA GPG key:
sudo dpkg -i cuda-keyring_1.1-1_all.deb
Update the Apt repository cache:
sudo apt-get update
Now, install DCGM:
sudo apt-get install -y datacenter-gpu-manager
Enable the DCGM systemd service (on reboot) and start it now:
sudo systemctl --now enable nvidia-dcgm
You should see output similar to this:
● dcgm.service - DCGM service Loaded: loaded (/usr/lib/systemd/system/dcgm.service; disabled; vendor preset: enabled) Active: active (running) since Mon 2020-10-12 12:18:57 PDT; 14s ago Main PID: 32847 (nv-hostengine) Tasks: 7 (limit: 39321) CGroup: /system.slice/dcgm.service └─32847 /usr/bin/nv-hostengine -n Oct 12 12:18:57 ubuntu1804 systemd[1]: Started DCGM service. Oct 12 12:18:58 ubuntu1804 nv-hostengine[32847]: DCGM initialized Oct 12 12:18:58 ubuntu1804 nv-hostengine[32847]: Host Engine Listener Started
To verify installation, use
dcgmi
to query the system. You should see a listing of all supported GPUs (and any NVSwitches) found in the system:dcgmi discovery -l
Output:
8 GPUs found. +--------+----------------------------------------------------------------------+ | GPU ID | Device Information | +--------+----------------------------------------------------------------------+ | 0 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:07:00.0 | | | Device UUID: GPU-1d82f4df-3cf9-150d-088b-52f18f8654e1 | +--------+----------------------------------------------------------------------+ | 1 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:0F:00.0 | | | Device UUID: GPU-94168100-c5d5-1c05-9005-26953dd598e7 | +--------+----------------------------------------------------------------------+ | 2 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:47:00.0 | | | Device UUID: GPU-9387e4b3-3640-0064-6b80-5ace1ee535f6 | +--------+----------------------------------------------------------------------+ | 3 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:4E:00.0 | | | Device UUID: GPU-cefd0e59-c486-c12f-418c-84ccd7a12bb2 | +--------+----------------------------------------------------------------------+ | 4 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:87:00.0 | | | Device UUID: GPU-1501b26d-f3e4-8501-421d-5a444b17eda8 | +--------+----------------------------------------------------------------------+ | 5 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:90:00.0 | | | Device UUID: GPU-f4180a63-1978-6c56-9903-ca5aac8af020 | +--------+----------------------------------------------------------------------+ | 6 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:B7:00.0 | | | Device UUID: GPU-8b354e3e-0145-6cfc-aec6-db2c28dae134 | +--------+----------------------------------------------------------------------+ | 7 | Name: A100-SXM4-40GB | | | PCI Bus ID: 00000000:BD:00.0 | | | Device UUID: GPU-a16e3b98-8be2-6a0c-7fac-9cb024dbc2df | +--------+----------------------------------------------------------------------+ 6 NvSwitches found. +-----------+ | Switch ID | +-----------+ | 11 | | 10 | | 13 | | 9 | | 12 | | 8 | +-----------+
As it doesn't need any persistence I've added it to the prometheus docker compose:
dcgm-exporter: # latest didn't work image: nvcr.io/nvidia/k8s/dcgm-exporter:3.3.6-3.4.2-ubuntu22.04 deploy: resources: reservations: devices: - capabilities: [gpu] restart: unless-stopped container_name: dcgm-exporter
And added the next scraping config in
prometheus.yml
- job_name: dcgm-exporter metrics_path: /metrics static_configs: - targets: - dcgm-exporter:9400
Adding alerts
Tweak the next alerts for your use case.
--- groups: - name: dcgm-alerts rules: - alert: GPUHighTemperature expr: DCGM_FI_DEV_GPU_TEMP > 80 for: 5m labels: severity: critical annotations: summary: "GPU High Temperature (instance {{ $labels.instance }})" description: "The GPU temperature is above 80°C for more than 5 minutes.\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" - alert: GPUMemoryUtilizationHigh expr: DCGM_FI_DEV_MEM_COPY_UTIL > 90 for: 10m labels: severity: warning annotations: summary: "GPU Memory Utilization High (instance {{ $labels.instance }})" description: "The GPU memory utilization is above 90% for more than 10 minutes.\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" - alert: GPUComputeUtilizationHigh expr: DCGM_FI_DEV_GPU_UTIL > 90 for: 10m labels: severity: warning annotations: summary: "GPU Compute Utilization High (instance {{ $labels.instance }})" description: "The GPU compute utilization is above 90% for more than 10 minutes.\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" - alert: GPUPowerUsageHigh expr: DCGM_FI_DEV_POWER_USAGE > 160 for: 5m labels: severity: warning annotations: summary: "GPU Power Usage High (instance {{ $labels.instance }})" description: "The GPU power usage is above 160W for more than 5 minutes.\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" - alert: GPUUnavailable expr: up{job="dcgm-exporter"} == 0 for: 5m labels: severity: critical annotations: summary: "GPU Unavailable (instance {{ $labels.instance }})" description: "The DCGM Exporter instance is down or unreachable for more than 5 minutes.\n LABELS: {{ $labels }}"
Adding a dashboard
I've tweaked this dashboard to simplify it. Check the article for the full json
Operating Systems⚑
Linux⚑
-
New: Linux loki alerts.
- alert: TooManyLogs expr: | sum by(hostname) (count_over_time({job="systemd-journal"} [1d])) / sum by(hostname) (count_over_time({job="systemd-journal"} [1d] offset 1d)) > 1.5 for: 0m labels: severity: warning annotations: summary: "The server {{ $labels.hostname}} is generating too many logs" - alert: TooFewLogs expr: | sum by(hostname) (count_over_time({job="systemd-journal"} [1d])) / sum by(hostname) (count_over_time({job="systemd-journal"} [1d] offset 1d)) < 0.5 for: 0m labels: severity: warning annotations: summary: "The server {{ $labels.hostname}} is generating too few logs"
Linux Snippets⚑
-
New: Makefile use bash instead of sh.
The program used as the shell is taken from the variable
SHELL
. If this variable is not set in your makefile, the program/bin/sh
is used as the shell.So put
SHELL := /bin/bash
at the top of your makefile, and you should be good to go. -
New: Recover the message of a commit if the command failed.
git commit
can fail for reasons such asgpg.commitsign = true
&&gpg
fails, or when running a pre-commit. Retrying the command opens a blank editor and the message seems to be lost.The message is saved though in
.git/COMMIT_EDITMSG
, so you can:git commit -m "$(cat .git/COMMIT_EDITMSG)"
Or in general (suitable for an alias for example):
git commit -m "$(cat "$(git rev-parse --git-dir)/COMMIT_EDITMSG)")"
-
New: Configure nginx to restrict methods.
server { listen 80; server_name yourdomain.com; location / { if ($request_method !~ ^(GET|POST)$ ) { return 405; } try_files $uri $uri/ =404; } }
-
New: Configure nginx location regexp to accept dashes.
location ~* /share/[\w-]+ { root /home/project_root; }
-
New: Configure nginx location to accept many paths.
location ~ ^/(static|media)/ { root /home/project_root; }
-
exiftool -all:all= /path/to/file
-
New: Get the size increment of a directory between two dates.
To see how much has a directory grown between two dates you can use:
find /path/to/directory -type f -newerat 2022-12-31 ! -newerat 2024-01-01 -printf "%s\\n" | awk '{s+=$1} END {print s}'
It finds all the files in that directory that were created in the 2023, it only prints their size in bytes and then it adds them all up.
-
New: Introduce detox.
detox cleans up filenames from the command line.
Installation:
apt-get install detox
Usage:
detox *
-
New: Rotate image with the command line.
If you want to overwrite in-place,
mogrify
from the ImageMagick suite seems to be the easiest way to achieve this:mogrify -rotate -90 *.jpg mogrify -rotate 90 *.jpg
-
New: Configure desktop icons in gnome.
Latest versions of gnome dont have desktop icons read this article to fix this
-
New: Send multiline messages with notify-send.
The title can't have new lines, but the body can.
feat(linux_snippets#Find BIOS version): Find BIOS versionnotify-send "Title" "This is the first line.\nAnd this is the second.")
dmidecode | less
-
New: Reboot server on kernel panic.
The
proc/sys/kernel/panic
file gives read/write access to the kernel variablepanic_timeout
. If this is zero, the kernel will loop on a panic; if nonzero it indicates that the kernel should autoreboot after this number of seconds. When you use the software watchdog device driver, the recommended setting is60
.To set the value add the next contents to the
/etc/sysctl.d/99-panic.conf
kernel.panic = 60
Or with an ansible task:
- name: Configure reboot on kernel panic become: true lineinfile: path: /etc/sysctl.d/99-panic.conf line: kernel.panic = 60 create: true state: present
-
New: Share a calculated value between github actions steps.
You need to set a step's output parameter. Note that the step will need an
id
to be defined to later retrieve the output value.echo "{name}={value}" >> "$GITHUB_OUTPUT"
For example:
- name: Set color id: color-selector run: echo "SELECTED_COLOR=green" >> "$GITHUB_OUTPUT" - name: Get color env: SELECTED_COLOR: ${{ steps.color-selector.outputs.SELECTED_COLOR }} run: echo "The selected color is $SELECTED_COLOR"
-
New: Split a zip into sizes with restricted size.
Something like:
zip -9 myfile.zip * zipsplit -n 250000000 myfile.zip
Would produce
myfile1.zip
,myfile2.zip
, etc., all independent of each other, and none larger than 250MB (in powers of ten).zipsplit
will even try to organize the contents so that each resulting archive is as close as possible to the maximum size. -
New: Find files that were modified between dates.
The best option is the
-newerXY
. The m and t flags can be used.m
The modification time of the file referencet
reference is interpreted directly as a time
So the solution is
find . -type f -newermt 20111222 \! -newermt 20111225
The lower bound in inclusive, and upper bound is exclusive, so I added 1 day to it. And it is recursive.
-
New: Add rofi launcher.
pass is a command line password store
Configure rofi launcher
- Save this script somewhere in your
$PATH
- Configure your window manager to launch it whenever you need a password.
- Save this script somewhere in your
-
ffprobe file.mkv
-
New: Clean the logs of a unit of the journal.
journalctl --vacuum-time=1s --unit=your.service
If you wish to clear all logs use
journalctl --vacuum-time=1s
-
New: Introduce Alacritty.
Alacritty is a modern terminal emulator that comes with sensible defaults, but allows for extensive configuration. By integrating with other applications, rather than reimplementing their functionality, it manages to provide a flexible set of features with high performance.
- Clone the repo
git clone https://github.com/alacritty/alacritty.git cd alacritty
- Install
rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- To make sure you have the right Rust compiler installed, run
rustup override set stable rustup update stable
- Install the dependencies
apt install cmake pkg-config libfreetype6-dev libfontconfig1-dev libxcb-xfixes0-dev libxkbcommon-dev python3
- Build the release If all goes well, this should place a binary at
cargo build --release
target/release/alacritty
- Move the binary to somewhere in your PATH
- Check the terminfo: To make sure Alacritty works correctly, either themv target/release/alacritty ~/.local/bin
alacritty
oralacritty-direct
terminfo must be used. Thealacritty
terminfo will be picked up automatically if it is installed. If the following command returns without any errors, thealacritty
terminfo is already installed:infocmp alacritty
If it is not present already, you can install it globally with the following command:
sudo tic -xe alacritty,alacritty-direct extra/alacritty.info
Alacritty's configuration file uses the TOML format. It doesn't create the config file for you, but it looks for one in
~/.config/alacrity/alacritty.toml
Not there yet - Support for ligatures
- Clone the repo
-
New: Set the vim filetype syntax in a comment.
Add somewhere in your file:
-
New: Export environment variables in a crontab.
If you need to expand the
PATH
in theory you can do it like this:PATH=$PATH:/usr/local/bin * * * * * /path/to/my/script
I've found however that sometimes this doesn't work and you need to specify it in the crontab line:
* * * * * PATH=$PATH:/usr/local/bin /path/to/my/script
-
Correction: Docker prune without removing the manual networks.
If you run the command
docker system prune
in conjunction with watchtower and manually defined networks you may run into the issue that the docker system prune acts just when the dockers are stopped and thus removing the networks, which will prevent the dockers to start. In those cases you can either make sure that docker system prune is never run when watchtower is doing the updates or you can split the command into the next script:date echo "Pruning the containers" docker container prune -f --filter "label!=prune=false" echo "Pruning the images" docker image prune -f --filter "label!=prune=false" echo "Pruning the volumes" docker volume prune -f
-
High I/O wait (
iowait
) on the CPU, especially at 50%, typically indicates that your system is spending a large portion of its time waiting for I/O operations (such as disk access) to complete. This can be caused by a variety of factors, including disk bottlenecks, overloaded storage systems, or inefficient applications making disk-intensive operations.Here’s a structured approach to debug and analyze high I/O wait on your server:
** Monitor disk I/O**
First, verify if disk I/O is indeed the cause. Tools like
iostat
,iotop
, anddstat
can give you an overview of disk activity:iostat
: This tool reports CPU and I/O statistics. You can install it withapt-get install sysstat
. Run the following command to check disk I/O stats:
Theiostat -x 1
-x
flag provides extended statistics, and1
means it will report every second. Look for high values in the%util
andawait
columns, which represent: -%util
: Percentage of time the disk is busy (ideally should be below 90% for most systems). -await
: Average time for I/O requests to complete.If either of these values is unusually high, it indicates that the disk subsystem is likely overloaded.
iotop
: If you want a more granular look at which processes are consuming disk I/O, useiotop
:
sudo iotop -o
This will show you the processes that are actively performing I/O operations.
dstat
: Another useful tool for monitoring disk I/O in real-time:
dstat -cdl 1
This shows CPU, disk, and load stats, refreshing every second. Pay attention to the
dsk/await
value.Check disk health Disk issues such as bad sectors or failing drives can also lead to high I/O wait times. To check the health of your disks:
- Use
smartctl
: This tool can give you a health check of your disks if they support S.M.A.R.T.
sudo smartctl -a /dev/sda
Check for any errors or warnings in the output. Particularly look for things like reallocated sectors or increasing "pending sectors."
dmesg
logs: Look at the system logs for disk errors or warnings:
dmesg | grep -i "error"
If there are frequent disk errors, it may be time to replace the disk or investigate hardware issues.
Look for disk saturation If the disk is saturated, no matter how fast the CPU is, it will be stuck waiting for data to come back from the disk. To further investigate disk saturation:
df -h
: Check if your disk partitions are full or close to full.
df -h
lsblk
: Check how your disks are partitioned and how much data is written to each partition:
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT
blktrace
: For advanced debugging, you can useblktrace
, which traces block layer events on your system.
sudo blktrace -d /dev/sda -o - | blkparse -i -
This will give you very detailed insights into how the system is interacting with the block device.
Check for heavy disk-intensive processes Identify processes that might be using excessive disk I/O. You can use tools like
iotop
(as mentioned earlier) orpidstat
to look for processes with high disk usage:pidstat
: Track per-process disk activity:
pidstat -d 1
This command will give you I/O statistics per process every second. Look for processes with high
I/O
values (r/s
andw/s
).top
orhtop
: Whiletop
orhtop
can show CPU usage, they can also show process-level disk activity. Focus on processes consuming high CPU or memory, as they might also be performing heavy I/O operations.
check file system issues Sometimes the file system itself can be the source of I/O bottlenecks. Check for any file system issues that might be causing high I/O wait.
- Check file system consistency: If you suspect the file system is causing issues (e.g., due to corruption), run a file system check. For
ext4
:
sudo fsck /dev/sda1
Ensure you unmount the disk first or do this in single-user mode.
- Check disk scheduling: Some disk schedulers (like
cfq
ordeadline
) might perform poorly depending on your workload. You can check the scheduler used by your disk with:
cat /sys/block/sda/queue/scheduler
You can change the scheduler with:
echo deadline > /sys/block/sda/queue/scheduler
This might improve disk performance, especially for certain workloads.
Examine system logs The system logs (
/var/log/syslog
or/var/log/messages
) may contain additional information about hardware issues, I/O bottlenecks, or kernel-related warnings:sudo tail -f /var/log/syslog
or
sudo tail -f /var/log/messages
Look for I/O or disk-related warnings or errors.
Consider hardware upgrades or tuning
- SSD vs HDD: If you're using HDDs, consider upgrading to SSDs. HDDs can be much slower in terms of I/O, especially if you have a high number of random read/write operations.
- RAID Configuration: If you are using RAID, check the RAID configuration and ensure it's properly tuned for performance (e.g., using RAID-10 for a good balance of speed and redundancy).
- Memory and CPU Tuning: If the server is swapping due to insufficient RAM, it can result in increased I/O wait. You might need to add more RAM or optimize the system to avoid excessive swapping.
Check for swapping issues Excessive swapping can contribute to high I/O wait times. If your system is swapping (which happens when physical RAM is exhausted), I/O wait spikes as the system reads from and writes to swap space on disk.
- Check swap usage:
free -h
If swap usage is high, you may need to add more physical RAM or optimize applications to reduce memory pressure.
-
New: Create a file with random data.
Of 3.5 GB
dd if=/dev/urandom of=random_file.bin bs=1M count=3584
-
New: Convert an html to a pdf.
Using weasyprint
Install it with
pip install weasyprint PyMuPDF
weasyprint input.html output.pdf
It gave me better result than
wkhtmltopdf
Using wkhtmltopdf To convert the given HTML into a PDF with proper styling and formatting using a simple method on Linux, you can use
wkhtmltopdf
with some custom options.First, ensure that you have
wkhtmltopdf
installed on your system. If not, install it using your package manager (e.g., Debian:sudo apt-get install wkhtmltopdf
).Then, convert the HTML to PDF using
wkhtmltopdf
with the following command:wkhtmltopdf --page-size A4 --margin-top 15mm --margin-bottom 15mm --encoding utf8 input.html output.pdf
In this command: -
--page-size A4
: Sets the paper size to A4. ---margin-top 15mm
and--margin-bottom 15mm
: Adds top and bottom margins of 15 mm to the PDF.After running the command, you should have a nicely formatted
output.pdf
file in your current directory. This method preserves most of the original HTML styling while providing a simple way to export it as a PDF on Linux.If you need to zoom in, you can use the
--zoom 1.2
flag. For this to work you need your css to be using theem
sizes. -
New: Format a drive to use a FAT32 system.
sudo mkfs.vfat -F 32 /dev/sdX
Replace /dev/sdX with your actual drive identifier
-
New: Get the newest file of a directory with nested directories and files.
find . -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -f2- -d" "
-
Correction: How to debug a CPU Throttling high alert.
If the docker is using less resources than the limits but they are still small (for example 0.1 CPUs) the issue may be that the CPU spikes are being throttle before they are shown in the CPU usage, the solution is then to increase the CPU limits
# Create a systemd service for a non-root user
To set up a systemd service as a non-root user, you can create a user-specific service file under your home directory. User services are defined in
~/.config/systemd/user/
and can be managed without root privileges. -
New: Check the docker images sorted by size.
docker images --format "{{.Repository}}:{{.Tag}}\t{{.Size}}" | sort -k2 -h
You can also use the builtin
docker system df -v
to get a better understanding of the usage of disk space.
Wezterm⚑
-
New: Introduce wezterm.
WezTerm is a powerful cross-platform terminal emulator and multiplexer implemented in Rust.
You can configure your system to use that APT repo by following these steps:
curl -fsSL https://apt.fury.io/wez/gpg.key | sudo gpg --yes --dearmor -o /usr/share/keyrings/wezterm-fury.gpg echo 'deb [signed-by=/usr/share/keyrings/wezterm-fury.gpg] https://apt.fury.io/wez/ * *' | sudo tee /etc/apt/sources.list.d/wezterm.list
Update your dependencies:
sudo apt update
Now you can install wezterm:
sudo apt install wezterm
or to install a nightly build:
Troubleshootingsudo apt install wezterm-nightly
Install from nightly.
References
Terminals⚑
-
New: Suggest to use nerd fonts.
If you're thinking of adding a new font, you should take a look at the nerd fonts as they patch developer targeted fonts with a high number of glyphs (icons). Specifically to add a high number of extra glyphs from popular ‘iconic fonts’ such as Font Awesome, Devicons, Octicons, and others.
For example you can get the
FiraCode Nerd Font
-
New: Introduce Rofi.
Rofi is a window switcher, application launcher and dmenu replacement.
sudo apt-get install rofi
Usage To launch rofi directly in a certain mode, specify a mode with
rofi -show <mode>
. To show the run dialog:rofi -show run
Or get the options from a script:
~/my_script.sh | rofi -dmenu
Specify an ordered, comma-separated list of modes to enable. Enabled modes can be changed at runtime. Default key is Ctrl+Tab. If no modes are specified, all configured modes will be enabled. To only show the run and ssh launcher:
rofi -modes "run,ssh" -show run
The modes to combine in combi mode. For syntax to
-combi-modes
, see-modes
. To get one merge view, of window,run, and ssh:rofi -show combi -combi-modes "window,run,ssh" -modes combi
The configuration lives at
~/.config/rofi/config.rasi
to create this file with the default conf run:rofi -dump-config > ~/.config/rofi/config.rasi
To run once:
rofi -show run -sorting-method fzf -matching fuzzy
To persist them change those same values in the configuration.
Theme changing To change the theme: - Choose the one you like most looking here - Run
rofi-theme-selector
to select it - Accept it withAlt + a
Plugins You can write your custom plugins. If you're on python using
python-rofi
seems to be the best option although it looks unmaintained.Some interesting examples are:
- Python based plugin
- Creation of nice menus
- Nice collection of possibilities
- Date picker
- Orgmode capture
Other interesting references are:
-
New: Do terminal comparison.
Pros:
Cons:
- The installation is difficult if you're not used to Rust.
- Doesn't support for ligatures so Fira Code with ligatures looks weird.
- Awful docs
- Difficult to keep updated
Pros:
- Built in python
- Supports ligatures
- Nice docs
- Easy installation
Cons:
- Vim bindings to move around and copy the buffer don't work well
- When you
sudo su
on a server you need to copy the~/.terminfo
otherwise the shell is broken - You need to replace
ssh
so that they copy that file.
Pros:
- Nice docs
Cons:
- Dev is enough narcissistic enough to not only add it in the name of the terminal but also to say his name as the main developer.
Gotify⚑
-
New: Complete installation.
- Create the data directories:
mkdir -p /data/config/gotify/ /data/gotify
- Assuming you're using an external proxy create the next docker compose in
/data/config/gotify
.
--- version: "3" services: gotify: image: gotify/server container_name: gotify networks: - swag env_file: - .env volumes: - gotify-data:/app/data networks: swag: external: name: swag volumes: gotify-data: driver: local driver_opts: type: none o: bind device: /data/gotify
With the next
.env
file:[Unit] Description=gotify Requires=docker.service After=docker.serviceGOTIFY_SERVER_SSL_ENABLED=false GOTIFY_DATABASE_DIALECT=sqlite3 GOTIFY_DATABASE_CONNECTION=data/gotify.db GOTIFY_DEFAULTUSER_NAME=admin GOTIFY_DEFAULTUSER_PASS=changeme GOTIFY_PASSSTRENGTH=10 GOTIFY_UPLOADEDIMAGESDIR=data/images GOTIFY_PLUGINSDIR=data/plugins GOTIFY_REGISTRATION=false ``` * Create the service by adding a file `gotify.service` into `/etc/systemd/system/`
[Service] Restart=always User=root Group=docker WorkingDirectory=/data/config/gotify TimeoutStartSec=100 RestartSec=2s ExecStart=/usr/bin/docker-compose -f docker-compose.yaml up ExecStop=/usr/bin/docker-compose -f docker-compose.yaml down
[Install] WantedBy=multi-user.target
* Copy the nginx configuration in your `site-confs` ``` server { listen 443 ssl; listen [::]:443 ssl; server_name gotify.*; include /config/nginx/ssl.conf; client_max_body_size 0; # enable for ldap auth (requires ldap-location.conf in the location block) #include /config/nginx/ldap-server.conf; # enable for Authelia (requires authelia-location.conf in the location block) #include /config/nginx/authelia-server.conf; location / { # enable the next two lines for http auth #auth_basic "Restricted"; #auth_basic_user_file /config/nginx/.htpasswd; # enable for ldap auth (requires ldap-server.conf in the server block) #include /config/nginx/ldap-location.conf; # enable for Authelia (requires authelia-server.conf in the server block) #include /config/nginx/authelia-location.conf; include /config/nginx/proxy.conf; include /config/nginx/resolver.conf; set $upstream_app gotify; set $upstream_port 80; set $upstream_proto http; proxy_pass $upstream_proto://$upstream_app:$upstream_port; } } ``` * Start the service `systemctl start gotify` * Restart the nginx service `systemctl restart swag` * Enable the service `systemctl enable gotify`. * Login with the `admin` user * Create a new user with admin permissions * Delete the `admin` user **Configuration** - [Android client](https://github.com/gotify/android) - Linux clients - [command line client](#command-line-client) - [Dunst client](https://github.com/ztpnk/gotify-dunst) - [gotify-desktop](https://github.com/desbma/gotify-desktop) - [rofi client](https://github.com/diddypod/rotify) **Connect it with Alertmanager** It's not trivial to connect it to Alertmanager([1](https://github.com/prometheus/alertmanager/issues/2120), [2](https://github.com/gotify/contrib/issues/21), [3](https://github.com/prometheus/alertmanager/issues/3729), [4](https://github.com/prometheus/alertmanager/issues/2120). The most popular way is to use [`alertmanager_gotify_bridge`](https://github.com/DRuggeri/alertmanager_gotify_bridge?tab=readme-ov-file). We need to tweak the docker-compose to add the bridge: ```yaml
Connect it with Authentik
Here are some guides to connect it to authentik. The problem is that the clients you want to use must support it
References
- Create the data directories:
HAProxy⚑
-
New: Automatically ban offending traffic.
Check these two posts:
-
New: Configure haproxy logs to be sent to loki.
In the
fronted
config add the next line:# For more options look at https://www.chrisk.de/blog/2023/06/haproxy-syslog-promtail-loki-grafana-logfmt/ log-format 'client_ip=%ci client_port=%cp frontend_name=%f backend_name=%b server_name=%s performance_metrics=%TR/%Tw/%Tc/%Tr/%Ta status_code=%ST bytes_read=%B termination_state=%tsc haproxy_metrics=%ac/%fc/%bc/%sc/%rc srv_queue=%sq backend_queue=%bq user_agent=%{+Q}[capture.req.hdr(0)] http_hostname=%{+Q}[capture.req.hdr(1)] http_version=%HV http_method=%HM http_request_uri="%HU"'
At the bottom of chrisk post is a table with all the available fields.
-
New: Reload haproxy.
- Check the config is alright
service haproxy configtest # Or /usr/sbin/haproxy -c -V -f /etc/haproxy/haproxy.cfg
- Reload the service
service haproxy reload
If you want to do a better reload you can drop the SYN before a restart, so that clients will resend this SYN until it reaches the new process.
iptables -I INPUT -p tcp --dport 80,443 --syn -j DROP sleep 1 service haproxy reload iptables -D INPUT -p tcp --dport 80,443 --syn -j DROP service haproxy reload
- Check the config is alright
journald⚑
-
New: Introduce journald.
journald is a system service that collects and stores logging data. It creates and maintains structured, indexed journals based on logging information that is received from a variety of sources:
- Kernel log messages, via kmsg
- Simple system log messages, via the
libc syslog
call - Structured system log messages via the native Journal API.
- Standard output and standard error of service units.
- Audit records, originating from the kernel audit subsystem.
The daemon will implicitly collect numerous metadata fields for each log messages in a secure and unfakeable way.
Journald provides a good out-of-the-box logging experience for systemd. The trade-off is, journald is a bit of a monolith, having everything from log storage and rotation, to log transport and search. Some would argue that syslog is more UNIX-y: more lenient, easier to integrate with other tools. Which was its main criticism to begin with. When the change was made not everyone agreed with the migration from syslog or the general approach systemd took with journald. But by now, systemd is adopted by most Linux distributions, and it includes journald as well. journald happily coexists with syslog daemons, as:
- Some syslog daemons can both read from and write to the journal
- journald exposes the syslog API
It provides lots of features, most importantly:
- Indexing. journald uses a binary storage for logs, where data is indexed. Lookups are much faster than with plain text files.
- Structured logging. Though it’s possible with syslog, too, it’s enforced here. Combined with indexing, it means you can easily filter specific logs (e.g. with a set priority, in a set timeframe).
- Access control. By default, storage files are split by user, with different permissions to each. As a regular user, you won’t see everything root sees, but you’ll see your own logs.
- Automatic log rotation. You can configure journald to keep logs only up to a space limit, or based on free space.
Syncthing⚑
-
New: Change the path of a folder.
- Shutdown Syncthing
- Edit the config file (
~/.config/syncthing/config.xml
) - Search and replace the path
- Start again syncthing
Wireguard⚑
-
New: Improve logging.
WireGuard doesn’t do any logging by default. If you use the WireGuard Linux kernel module (on kernel versions 5.6 or newer), you can turn on WireGuard’s dyndbg logging, which sends log messages to the kernel message buffer, kmsg. You can then use the standard dmesg utility to read these messages. Also, many Linux systems have a logging daemon like rsyslogd or journald that automatically captures and stores these messages.
First, enable WireGuard
dyndbg
logging with the following commands:modprobe wireguard echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
Once you do that, you’ll be able to see WireGuard log messages in the kernel message facility, if your system is set up with
rsyslogd
,journald
, or a similar logging daemon. Withrsyslogd
, check the/var/log/kern.log
or/var/log/messages
file. Withjournald
, runjournalctl -ek
. -
New: Monitor wireguard.
-
New: Check the status of the tunnel.
One method is to do ping between VPN IP addresses or run command
wg show`` from the server or from the client. Below you can see
wg show`` command output where VPN is not up.$: wg show interface: wg0 public key: qZ7+xNeXCjKdRNM33Diohj2Y/KSOXwvFfgTS1LRx+EE= private key: (hidden) listening port: 45703 peer: mhLzGkqD1JujPjEfZ6gkbusf3sfFzy+1KXBwVNBRBHs= endpoint: 3.133.147.235:51820 allowed ips: 10.100.100.1/32 transfer: 0 B received, 592 B sent persistent keepalive: every 21 seconds
The below output from the
wg show
command indicates the VPN link is up. See the line withlast handshake time
$: wg show interface: wg0 public key: qZ7+xNeXCjKdRNM33Diohj2Y/KSOXwvFfgTS1LRx+EE= private key: (hidden) listening port: 49785 peer: 6lf4SymMbY+WboI4jEsM+P9DhogzebSULrkFowDTt0M= endpoint: 3.133.147.235:51820 allowed ips: 10.100.100.1/32 latest handshake: 14 seconds ago transfer: 732 B received, 820 B sent persistent keepalive: every 21 seconds
Android⚑
GrapheneOS⚑
-
New: Disable Bluetooth and Wifi once it's disconnected.
If you don't want to go spreading your SSIDs you can configure graphene to disable wifi and bluetooth X minutes after loosing connection.
For Wifi:
- Go to Settings > network & internet > internet > network preferences
- Select Turn off Wi-Fi automatically, for example after 2 minutes
For Bluetooth:
- Go to Settings > connected devices > connection preferences> bluetooth
- Select Turn Bluetooth off automatically, for example after 2 minutes
GadgetBridge⚑
-
Correction: Installation on GrapheneOS.
On GrapheneOS you may need to enable the restricted permissions
Android SDK Platform tools⚑
-
New: Introduce android_sdk.
Android SDK Platform tools is a component for the Android SDK. It includes tools that interface with the Android platform, primarily adb and fastboot.
While many Linux distributions already package Android Platform Tools (for example
android-platform-tools-base
on Debian), it is preferable to install the most recent version from the official website. Packaged versions might be outdated and incompatible with most recent Android handsets.- Download the latest toolset
- Extract it somewhere in your filesystem
- Create links to the programs you want to use in your
$PATH
Next you will need to enable debugging on the Android device you are testing. Please follow the official instructions on how to do so.
Usage
Connecting over USB
To use
adb
with a device connected over USB, you must enable USB debugging in the device system settings, under Developer options. On Android 4.2 (API level 17) and higher, the Developer options screen is hidden by default.Enable the Developer options
To make it visible, enable Developer options. On Android 4.1 and lower, the Developer options screen is available by default. On Android 4.2 and higher, you must enable this screen.
- On your device, find the Build number option (Settings > About phone > Build number)
- Tap the Build Number option seven times until you see the message You are now a developer! This enables developer options on your device.
- Return to the previous screen to find Developer options at the bottom.
Enable USB debugging
Before you can use the debugger and other tools, you need to enable USB debugging, which allows Android Studio and other SDK tools to recognize your device when connected via USB.
Enable USB debugging in the device system settings under Developer options. You can find this option in one of the following locations, depending on your Android version:
- Android 9 (API level 28) and higher: Settings > System > Advanced > Developer Options > USB debugging
- Android 8.0.0 (API level 26) and Android 8.1.0 (API level 27): Settings > System > Developer Options > USB debugging
- Android 7.1 (API level 25) and lower: Settings > Developer Options > USB debugging
Test it works
If everything is configured appropriately you should see your device when launching the command
adb devices
.Create udev rules if it fails
If you see the next error:
failed to open device: Access denied (insufficient permissions) * failed to start daemon adb: failed to check server version: cannot connect to daemon
It indicates an issue with permissions when
adb
tries to communicate with the device via USB. Here are some steps you can take to resolve this issue:- Check USB permissions
- Ensure that you have the necessary permissions to access the USB device. If you're running on Linux, check if the device has appropriate udev rules.
-
You can try adding your user to the
plugdev
group:sudo usermod -aG plugdev $USER
-
Make sure you have a
udev
rule for Android devices in/etc/udev/rules.d/
. If not, you can create one by adding a file like51-android.rules
:sudo touch /etc/udev/rules.d/51-android.rules
-
Add this line to the file to grant access to Android devices:
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"
-
Reload the
udev
rules:sudo udevadm control --reload-rules sudo service udev restart
-
Unplug and reconnect the USB device. References
- Home
ICSx5⚑
Signal⚑
-
New: Use the Molly FOSS android client.
Molly is an independent Signal fork for Android. The advantages are:
- Contains no proprietary blobs, unlike Signal.
- Protects database with passphrase encryption.
- Locks down the app automatically when you are gone for a set period of time.
- Securely shreds sensitive data from RAM.
- Automatic backups on a daily or weekly basis.
- Supports SOCKS proxy and Tor via Orbot.
Note, the migration should be done when the available Molly version is equal to or later than the currently installed Signal app version.
- Verify your Signal backup passphrase. In the Signal app: Settings > Chats > Chat backups > Verify backup passphrase.
- Optionally, put your phone offline (enable airplane mode or disable data services) until after Signal is uninstalled in step 5. This will prevent the possibility of losing any Signal messages that are received during or after the backup is created.
- Create a Signal backup. In the Signal app, go to Settings > Chats > Chat backups > Create backup.
- Uninstall the Signal app. Now you can put your phone back online (disable airplane mode or re-enable data services).
- Install the Molly or Molly-FOSS app.
- Open the Molly app. Enable database encryption if desired. As soon as the option is given, tap Transfer or restore account. Answer any permissions questions.
- Choose to Restore from backup and tap Choose backup. Navigate to your Signal backup location (Signal/Backups/, by default) and choose the backup that was created in step 3.
- Check the backup details and then tap Restore backup to confirm. Enter the backup passphrase when requested.
- If asked, choose a new folder for backup storage. Or choose Not Now and do it later.
Consider also:
- Any previously linked devices will need to be re-linked. Go to Settings > Linked devices in the Molly app. If Signal Desktop is not detecting that it is no longer linked, try restarting it.
- Verify your Molly backup settings and passphrase at Settings > Chats > Chat backups (to change the backup folder, disable and then enable backups). Tap Create backup to create your first Molly backup.
- When you are satisfied that Molly is working, you may want to delete the old Signal backups (in Signal/Backups, by default).
Arts⚑
Cooking⚑
-
New: Comer naranja por la noche.
"Por la mañana oro, al mediodía plata y por la noche mata". Así de tajante se muestra el refranero español con respecto a la naranja y el melón. La naranja es una fruta ácida y por esta razón es recomendable evitar su consumo por la noche. Y es que este alto nivel de acidez puede provocar una digestión lenta y sensación de acidez y ardor en el estómago. Directamente relacionado con estos síntomas puede sobrevenir dolor de estómago y esto conducirnos a un descanso poco placentero durante la noche. Así, lo mejor es evitar su consumo a última hora del día. Si encima le añades unos trocitos de chocolate negro dificultarás el tránsito intestinal (por las grasas) y harás que te cueste más conciliar el sueño ya que es estimulante. Pero qué rico sabe de postre de cena xD.
Estas consecuencias varían en función de las personas y de su sistema digestivo. Así, este alimento sí puede resultar saludable para ciertas personas sin problemas gástricos y un alto nivel de tolerancia a este alimento.
Lo más aconsejable es consumirla por la mañana. Tomar una naranja a primera hora del día te aportará una buena dosis de vitamina C y fibra.
Siempre es recomendable tomar la fruta directamente y evitar los zumos, aunque sean naturales. De esta forma nos sentiremos más saciados y añadiremos más fibra y menos azúcares a nuestra dieta.
Además, la acidez de las naranjas que podría perjudicarnos por la noche es perfectamente tolerable por la mañana incluso si la tomamos en ayunas. De esta forma, podremos ir asimilando sus propiedades durante todo el día.
Estas frutas tienen un gran efecto antioxidante. Además, su gran contenido en vitamina C es un refuerzo inigualable para el sistema inmunológico y ayuda a combatir la anemia. También es un remedio ideal para tratar los resfriados y las gripes.
-
New: Por qué no vale con quitar la parte del moho de los alimentos.
Cleaning⚑
Cleaning tips⚑
-
New: Cleaning car headlights.
If you need to clean the car headlights you can use a mixture of one squeezed lemon and two spoonfuls of baking soda
Parkour⚑
-
New: Warming up.
Never do static stretches if you're cold, it's better to do dynamic stretches.
Take the joints through rotations
- Head:
- Nod 10 times
- Say no 10 times
- Ear shoulder 10 times
-
Circles 10 times each direction
-
Shoulders
- Circles back 10 times
-
Circles forward 10 times
-
Elbows
-
Circles 10 each direction
-
Wrists:
-
Circle 10 each direction
-
Chest:
- Chest out/in 10 times
- Chest one side to the other 10 times
-
Chest in circles
-
Hips:
- Circles 10 each direction
-
Figure eight 10 times each direction
-
Knees:
- Circular rotations 10 each direction feet and knees together
- 10 ups and downs with knees together
-
Circular rotations 10 each direction feet waist width
-
Ankles:
- Circular 10 rotations each direction
Light exercises
- 10 steps forward of walking on your toes, 10 back
- 10 steps forward of walking on your toes feet rotated outwards, 10 back
- 10 steps forward of walking on your toes feet rotated inwards, 10 back
- 10 steps forward of walking on your heels feet rotated outwards, 10 back
-
10 steps forward of walking on your heels feet rotated inwards, 10 back
-
2 x 10 x Side step, carry the leg up (from out to in) while you turn 180, keep on moving on that direction
- 2 x 10 x Front step carrying the leg up (from in to out)while you turn 45, then side step, keep on moving on that direction
- 10 light skips on one leg: while walking forward lift your knee and arms and do a slight jump
- 10 steps with high knees
- 10 steps with heel to butt
-
10 side shuffles (like basketball defense)
-
5 lunges forward, 5 backwards
-
10 rollups and downs from standing position
- 5 push-ups
- 10 rotations from the pushup position on each direction with straigth arms
- 5 push-ups
- 10 rotations from the pushup position on each direction with shoulders at ankle level
-
3 downward monkeys: from piramid do a low pushup and go to cobra, then a pushup
-
10 steps forward walking on all fours
Strengthen your knees
Follow there steps
Transit to the parkour place
Go by bike, skate, jogging to the parkour place
Music⚑
Sister Rosetta Tharpe⚑
-
New: Introduce Sister Rosetta Tharpe.
Sister Rosetta Tharpe was a visionary, born in 1915 she started shredding the guitar in ways that did not exist in that time. Yes, she founded Rock and Roll. It's lovely to see a gospel singer with an electrical guitar.
In this video you'll be able to understand how awesome she ws.
Videos:
Drawing⚑
Languages⚑
Castellano⚑
-
New: El agua o la agua?.
El sustantivo agua es de género femenino, pero tiene la particularidad de comenzar por /a/ tónica (la vocal tónica de una palabra es aquella en la que recae el acento de intensidad: [água]). Por razones de fonética histórica, este tipo de palabras seleccionan en singular la forma
el
del artículo, en lugar de la forma femenina normalla
. Esta regla solo opera cuando el artículo antecede inmediatamente al sustantivo, de ahí que digamos el agua, el área, el hacha; pero, si entre el artículo y el sustantivo se interpone otra palabra, la regla queda sin efecto, de ahí que digamos la misma agua, la extensa área, la afilada hacha. Puesto que estas palabras son femeninas, los adjetivos deben concordar siempre en femenino: el agua clara, el área extensa, el hacha afilada (y no el agua claro, el área extenso, el hacha afilado).Por su parte, el indefinido
una
toma generalmente la formaun
cuando antecede inmediatamente a sustantivos femeninos que comienzan por /a/ tónica: un área, un hacha, un águila (si bien no es incorrecto, aunque sí poco frecuente, utilizar la forma plena una: una área, una hacha, una águila). Asimismo, los indefinidosalguna
yninguna
pueden adoptar en estos casos las formas apocopadas (algún alma, ningún alma) o mantener las formas plenas (alguna alma, ninguna alma).Al tratarse de sustantivos femeninos, con los demostrativos este, ese, aquel o con cualquier otro adjetivo determinativo, como todo, mucho, poco, otro, etc., deben usarse las formas femeninas correspondientes: esta hacha, aquella misma arma, toda el agua, mucha hambre, etc. (y no este hacha, aquel mismo arma, todo el agua, mucho hambre, etc.)
Galego⚑
- New: Add some galego vocabulary.
-
New: Introduce galego.
O galego é unha lingua indoeuropea que pertence á póla de linguas románicas. É a lingua propia de Galiza, onde é falada por uns 2.4 millóns de galegas. Á parte de en Galiza, a lingua falase tamén en territórios limítrofes con esta comunidade, ainda que sen estatuto de oficialidade, asi como pola diáspora galega que emigrou a outras partes do estado español, América latina, os Estados Unidos, Suíza e outros países do Europa.
-
New: Te e che. Trucos para saber diferencialos.
En galego temos dúas formas para o pronome átono da segunda persoa do singular: te e che.
O pronome te ten a función de complemento directo (CD) e o pronome che de complemento indirecto (CI).
Cando se utiliza o pronome te?
O pronome te utilízase cando ten a función de CD, propio dos verbos transitivos, xa que alude ao ser ou ao obxecto sobre o que recae a acción verbal.
Se convertemos a oración en pasiva, o CD pasa a ser o suxeito. Por exemplo:
Vinte na cafetería / Ti fuches visto por min na cafetería.
Cando se utiliza o pronome che?
O pronome che utilízase cando ten a función de CI, xa que indica o destinatario da acción expresada polo verbo. Por exemplo:
Díxenche a verdade.
Compreiche unhas lambonadas.
Truco para saber diferencialos
Un truco moi rápido para diferenciarmos os pronomes te e che é substituír eses pronomes de segunda persoa polos de terceira.
Se podemos cambiar ese pronome por o/lo/no ou a/la/na, quere dicir que o pronome vai ser de CD. Polo tanto, temos que poñer te.
Saudeite onte pola rúa / Saudeino onte pola rúa.
Chameite por teléfono / Chameina por teléfono.
Se podemos substituílo por un lle, significa que é un pronome de CI e que debemos utilizar o che.
Lévoche mañá os apuntamentos / Lévolle mañá os apuntamentos.
Collinche as entradas do concerto / Collinlle as entradas do concerto.
-
New: Uso de asemade.
Asemade pode utilizarse como adverbio cando ten o significado de ‘ao mesmo tempo’ ou ‘simultaneamente’. Ainda que normalmente úsase no registro culto, non utilizalo na fala.
- Non se pode comer e falar asemade.
- Non podes facer os deberes e ver a televisión asemade, pois non te concentras.
Tamén se pode utilizar como conxunción co significado de ‘tan pronto como’.
- Foi o primeiro que vimos asemade entramos.
- Recoñecino asemade o vin.
É incorrecto empregar asemade como sinónimo de tamén, ademais ou igualmente.
-
New: Referencias e libros de gramática.
Referencias:
- Dicionario
- Traductor
- Juego Pensatermos
- Conxugador de verbos
- Celga-1 materiais
- Conversas do fenómeno das persoas neofalantes e o futuro do idioma
Libros gramática:
Science⚑
Artificial Intelligence⚑
-
New: Add aider tool.
- Aider lets you pair program with LLMs, to edit code in your local git repository. Start a new project or work with an existing git repo. Aider works best with GPT-4o & Claude 3.5 Sonnet and can connect to almost any LLM.
Whisper⚑
-
New: Introduce whisper.
Web interfaces
Command line tools
References
Coding by Voice⚑
-
New: Introduce Coding by voice.
Coding by voice command requires two kinds of software: a speech-recognition engine and a platform for voice coding. Dragon from Nuance, a speech-recognition software developer in Burlington, Massachusetts, is an advanced engine and is widely used for programming by voice. On the platform side, VoiceCode by Ben Meyer and Talon by Ryan Hileman (both are for Mac OS only) are popular.
Coding by voice platforms:
Two platforms for voice programming are Caster and Aenea, the latter of which runs on Linux. Both are free and open source, and enable voice-programming functionality in Dragonfly, which is an open-source Python framework that links actions with voice commands detected by a speech-recognition engine. Saphra tried Dragonfly, but found that setup required more use of her hands than she could tolerate.
All of these platforms for voice command work independently of coding language and text editor, and so can also be used for tasks outside programming. Pimentel, for instance, uses voice recognition to write e-mails, which he finds easier, faster and more natural than typing.
To the untrained ear, coding by voice command sounds like staccato bursts of a secret language. Rudd’s video is full of terms like ‘slap’ (hit return), ‘sup’ (search up) and ‘mara’ (mark paragraph).
Unlike virtual personal assistants such as Apple’s Siri or Google’s Alexa, VoiceCode and Talon don’t do natural-language processing, so spoken instructions have to precisely match the commands that the system already knows. But both platforms use continuous command recognition, so users needn’t pause between commands, as Siri and Alexa require.
VoiceCode commands typically use words not in the English language, because if you use an English word as a command, such as ‘return’, it means you can never type out that word. By contrast, Talon, Aenea and Caster feature dynamic grammar, a tool that constantly updates which words the software can recognize on the basis of which applications are open. This means users can give English words as commands without causing confusion.
In addition to voice recognition, Talon can also replace a computer mouse with eye tracking, which requires a Tobii 4c eye tracker (US$150). Other eye-mousing systems generally require both the eye tracker and head-tracking hardware, such as the TrackIR from NaturalPoint. “I want to make fully hands-free use of every part of a desktop computer a thing,” says Hileman. Other mouse replacements also exist; Pimentel uses one called SmartNav.
Voice command requires at least a decent headset or microphone. Many users choose a unidirectional microphone so that others can talk to them while they are dictating code. One such mic, a cardioid mic, requires special equipment to supply power, and hardware costs can reach $400, says Pimentel.
The software can cost several hundred dollars too. The speech-recognition engine Dragon Professional costs $300, as does VoiceCode. Caster and Aenea are free and open source. Talon is available for free, but requires a separate speech-recognition engine. A beta version of Talon that includes a built-in speech-recognition engine is currently available to Hileman’s Patreon supporters for $15 per month.
Whether or not users have RSI, it can be difficult and frustrating to start programming by voice. It took a month and a half for Pimentel to get up to speed, he says, and there were days when he was ready to throw in the towel. He printed out 40 pages of commands and forced himself to look at them until he learnt them. Saphra needed two months of coding, a little every day, before she felt that it was a “perfectly enjoyable experience and I could see myself doing this for a living”.
After the initial learning curve, users often create custom prompts for commonly used commands as the need arises.
Data Analysis⚑
Parsers⚑
-
Parsers are a whole world. I kind of feel a bit lost right now and I'm searching for good books on the topic. So far I've found:
Pros: - Pleasant to read - Doesn't use external tools, you implement it from scratch. - Multiple format: EPUB, PDF, web - You can read it for free - Cute drawings <3
Cons: - Code snippets are on Java and C - Doesn't use external tools, you implement it from scratch - It's long
- Compilers: Principles, Techniques, and Tools by Aho, Alfred V. & Monica S. Lam & Ravi Sethi & Jeffrey D. Ullman
Pros: - EPUB
Cons: - Code snippets are on C++
- Parsing Techniques: A Practical Guide by Dick Grune and Ceriel J.H Jacobs
Pros: - Gives an overview of many grammars and parsers
Cons: - Only in PDF - It's long - Too focused on the theory, despite the name xD
Other⚑
-
New: Inhibit rules between times.
To prevent some alerts to be sent between some hours you can use the
time_intervals
alertmanager configuration.This can be useful for example if your backup system triggers some alerts that you don't need to act on.
route: receiver: 'email' group_by: [job, alertname, severity] group_wait: 5m group_interval: 5m repeat_interval: 12h routes: - receiver: 'email' matchers: - alertname =~ "HostCpuHighIowait|HostContextSwitching|HostUnusualDiskWriteRate" - hostname = backup_server mute_time_intervals: - night time_intervals: - name: night time_intervals: - times: - start_time: 02:00 end_time: 07:00
-
New: Thoughts on the reviews themselves.
-
Keep It Simple: It's important for the process to be light enough that you want to actually do it, so you see it as a help instead of a burden. It's always better to do a small and quick review rather than nothing at all. At the start of the review analyze yourself to assess how much energy do you have and decide which steps of the review you want to do.
-
Review approaches: In the past I used the life logging tools to analyze the past in order to understand what I achieved and take it as a base to learn from my mistakes. It was useful when I needed the endorphines boost of seeing all the progress done. Once I assumed that progress speed and understood that we always do the best we can given how we are, I started to feel that the review process was too cumbersome and that it was holding me into the past.
Nowadays I try not to look back but forward, analyze the present: how I feel, how's the environment around me, and how can I tweak both to fulfill my life goals. This approach leads to less reviewing of achievements and logs and more introspection, thinking and imagining. Which although may be slower to correct mistakes of the past, will surely make you live closer to the utopy.
The reviews below then follow that second approach.
- Personal alive reviews: Reviews have to reflect ourselves, and we change continuously, so take for granted that your review is going to change.
I've gone for full blown reviews of locking myself up for a week to not doing reviews for months.
This article represent the guidelines I follow to do my life review. It may seem a lot to you or may be very simple. Please take it as a base or maybe to get some ideas and then create your own that fits your needs.
-
-
New: Update the Month review process.
-
New: When to do the trimester reviews.
As with moth reviews, it's interesting to do analysis at representative moments. It gives it an emotional weight. You can for example use the solstices or my personal version of the solstices:
-
Spring analysis (1st of March): For me the spring is the real start of the year, it's when life explodes after the stillness of the winter. The sun starts to set later enough so that you have light in the afternoons, the climate gets warmer thus inviting you to be more outside, the nature is blooming new leaves and flowers. It is then a moment to build new projects and set the current year on track.
-
Summer analysis (1st of June): I hate heat, so summer is a moment of retreat. Everyone temporarily stop their lives, we go on holidays and all social projects slow their pace. Even the news have even less interesting things to report. It's so hot outside that some of us seek the cold refuge of home or remote holiday places. Days are long and people love to hang out till late, so usually you wake up later, thus having less time to actually do stuff. Even in the moments when you are alone the heat drains your energy to be productive. It is then a moment to relax and gather forces for the next trimester. It's also perfect to develop easy and chill personal projects that have been forgotten in a drawer. Lower your expectations and just flow with what your body asks you.
-
Autumn analysis (1st of September): September it's another key moment for many people. We have it hardcoded in our life since we were children as it was the start of school. People feel energized after the summer holidays and are eager to get back to their lives and stopped projects. You're already 6 months into the year, so it's a good moment to review your year plan and decide how you want to invest your energy reserves.
-
Winter analysis (1st of December): December is the cue that the year is coming to an end. The days grow shorter and colder, they basically invite you to enjoy a cup of tea under a blanket. It is then a good time to get into your cave and do an introspection analysis on the whole year and prepare the ground for the coming year. Some of the goals of this season are:
- Think everything you need to guarantee a good, solid and powerful spring start.
- Do the year review to adjust your principles.
The year is then divided in two sets of an expansion trimester and a retreat one. We can use this information to adjust our life plan accordingly. In the expansion trimester we could invest more energies in the planning, and in the retreat ones we can do more throughout reviews.
-
-
New: The principle documents.
Principle documents for me are orgmode documents where I think about the principle itself. It acts both as a way of understanding it and evolving my idea around it, and to build the roadmap to materialize the principle's path.
Without ever having created one I feel that it makes sense to make the reflection part public in the blue book, while I keep for myself the private one. This may also change between principles.
-
New: The life path document.
The life path document is an orgmode document where I think about what I want to do with my life and how. It's the highest level of abstraction of the life management system.
The structure so far is as follows:
* Life path ** {year} *** Principles of {season} {year} {Notes on the season} - Principle 1 - Principle 2 ... **** Objectives of {month} {year} - [-] Objective 1 - [X] SubObjective 1 - [ ] SubObjective 2 - [ ] Objective 2 - [ ] ...
Where the principles are usually links to principle documents and the objectives links to tasks.
-
New: Trimester prepare.
The trimester review requires an analysis that doesn't fill in a day session. It requires slow thinking over some time. So I'm creating a task 10 days before the actual review to start thinking about the next trimester. Whether it's ideas, plans, desires, objectives, or principles.
Is useful for that document to be available wherever you go, so that in any spare time you can pop it up and continue with the train of thought.
Doing the reflection without seeing your life path prevents you from being tainted by it, thus representing the real you of right now.
On the day to actually do the review, follow the steps of the Month review prepare adjusting them to the trimester case.
-
New: Update the month planning.
Decide the month objectives:
Create the month objectives in your roadmap file after addressing each element of:
- Your last month review document.
- The trimester objectives of your roadmap.
Once they are ready, copy them to the description of your
todo.org
file. That way you'll see it every day.For each of your month objectives:
- Decide whether it makes sense to address it this month. If not, archive it
- Create a clear plan of action for this month on that objective
- Define the todo of each device (mobile, tablet, laptop)
- Tweak your things to think about list.
- Tweak your reading list.
- Tweak your week distribution (what to do in each day)
- If you selected maintenance week days tweak the priorities on your maintenance list.
- If you selected improvement week days tweak the priorities on your improvements list.
- Tweak your habit manager system.
-
New: Use deadlines.
Identify hard deadlines: Add a warning days before the deadline to make sure you're reminded until it's done.
-
New: Add 2024 Hidden Cup 5 awesome match.
-
New: Prometheus metrics.
-
Correction: Tweak the month planning.
Add the next steps:
- Clean your agenda and get an feeling of the busyness of the month:
- Open the orgmode month view agenda and clean it
- Read the rest of your calendars
Then reorder the objectives in order of priority. Try to have at least one objective that improves your life.
- For each of your month and trimester objectives:
- Decide whether it makes sense to address it this month. If not, mark it as inactive
-
Create a clear plan of action for this month on that objective.
- Reorder the projects as needed
- Mark as INACTIVE the ones that you don't feel need to be focused on this month.
-
Refine the roadmap of each of the selected areas (change this to the trimestral planning)
- Select at least one coding project in case you enter in programming mode
- Clean your mobile browser tabs
-
Correction: Deprecate not-by-ai.
As they have introduced pricing, which makes no sense, and we had a discussion that using that badge it's a nice way to tell the AI which content to use and which not to
-
Reorganization: Into roadmap_adjustment.