<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jordan Haines</title>
    <description>The latest articles on Forem by Jordan Haines (@jordanahaines).</description>
    <link>https://forem.com/jordanahaines</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F33957%2Fe6a76c2f-cef1-4665-840b-b9ca3c75f956.jpeg</url>
      <title>Forem: Jordan Haines</title>
      <link>https://forem.com/jordanahaines</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jordanahaines"/>
    <language>en</language>
    <item>
      <title>Just use this Next.js Eslint Configuration</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Sun, 12 Jan 2025 18:59:34 +0000</pubDate>
      <link>https://forem.com/jordanahaines/just-use-this-nextjs-eslint-configuration-540</link>
      <guid>https://forem.com/jordanahaines/just-use-this-nextjs-eslint-configuration-540</guid>
      <description>&lt;h1&gt;
  
  
  Here, Take this Configuration
&lt;/h1&gt;

&lt;p&gt;I get it. If you're just here to find a good, working &lt;a href="https://eslint.org/" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt; configuration for a NextJS project, then look no further. Copy what's below. Although, it's probably out of date, so you can find a version that's been updated since I published this post in my open &lt;a href="https://learnbuildteach.substack.com/p/building-historio-episode-0?r=10737" rel="noopener noreferrer"&gt;source project Historio&lt;/a&gt; &lt;a href="https://github.com/jordanahaines/historio/blob/main/historio/eslint.config.mjs" rel="noopener noreferrer"&gt;➡️ in Github here ⬅️&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eslint.config.mjs&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Run this command to install necessary npm packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i --save eslint typescript-eslint eslint-config-next eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-tailwindcss eslint-plugin-unicorn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Why Lint
&lt;/h1&gt;

&lt;p&gt;When starting a new project, the very first thing I do is write some really sloppy, hacky, and messy code that (mostly) just works. I'm trying to validate an idea; to get a product off the ground.&lt;/p&gt;

&lt;p&gt;This lasts all of about 2 pull requests before it gets out of hand, and I need some rules. You can see the exact moment in &lt;a href="https://historio.app" rel="noopener noreferrer"&gt;Historio&lt;/a&gt; where I realized inconsistent code was holding me back &lt;a href="https://github.com/jordanahaines/historio/pull/9" rel="noopener noreferrer"&gt;in this pull request&lt;/a&gt; where linting was first applied.&lt;/p&gt;

&lt;p&gt;Having conventions on shared projects helps keep everyone aligned and working in the same direction. Conventions allow us to focus code reviews and discussion on the logic problems that matter instead of the semicolon placement or tab size that - frankly - only matter &lt;em&gt;because&lt;/em&gt; they’re a distraction. Written conventions for a team, starting with a style guide but encompassing any rules that keep engineers focused on engineering, are thus essential.&lt;/p&gt;

&lt;p&gt;The benefits to a team may be obvious, but even individual projects deserve conventions. Perhaps even stricter ones. Indeed, when I’m working on a project alone, I tend to codify a larger set of conventions than I would with a team simply because I get to do everything exactly my way. It's unclear if this is software engineering bliss or OCD. Every convention eliminates decisions I would otherwise have to make in the future (often many times over). Further, without a colleague to review every line of code I write, conventions help keep me consistent and tidy even when I really want to move fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Arrived at this Configuration
&lt;/h2&gt;

&lt;p&gt;Other coders have thought longer and harder about conventions than I ever want to. When spinning up a new linting configuration or style guide, I look to what already exists. For my stack, this includes the following &lt;a href="https://eslint.org/docs/latest/use/configure/plugins" rel="noopener noreferrer"&gt;ESLint Plugins&lt;/a&gt;. For each, I start with their recommended config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@vercel/style-guide" rel="noopener noreferrer"&gt;Vercel's rules for NextJS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/eslint-plugin-react" rel="noopener noreferrer"&gt;Official React eslint plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://typescript-eslint.io/getting-started/" rel="noopener noreferrer"&gt;Typescript ESLint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/eslint-plugin-tailwindcss" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://orm.drizzle.team/docs/eslint-plugin" rel="noopener noreferrer"&gt;Drizzle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/sindresorhus/eslint-plugin-unicorn" rel="noopener noreferrer"&gt;Unicorn&lt;/a&gt; - This is the big one,&lt;/strong&gt; with lots of rules and opinions on general typescript styling outside of conventions for the specific frameworks covered by the plugins above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, here are a few plugins I evaluated but decided &lt;em&gt;not&lt;/em&gt; to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb" rel="noopener noreferrer"&gt;AirBbB&lt;/a&gt;. I usually start here, but Vercel's config covered most of what I care about for both React and Node code styling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/xojs/eslint-config-xo/tree/main" rel="noopener noreferrer"&gt;XO&lt;/a&gt;. This feels like the &lt;a href="https://github.com/psf/black" rel="noopener noreferrer"&gt;Black&lt;/a&gt; of Typescript linting. XO has styles for &lt;em&gt;everything&lt;/em&gt; and is &lt;em&gt;very opinionated&lt;/em&gt;. This can be nice, because it takes a lot of code style decisions off of your plate. But beware it can be cumbersome to implement in the middle of a project because it will require extensive reformatting. In lieu of XO, I found &lt;a href="https://github.com/sindresorhus/eslint-plugin-unicorn" rel="noopener noreferrer"&gt;Unicorn&lt;/a&gt; opinionated enough and more immediately useful.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Note on ESLint Config
&lt;/h2&gt;

&lt;p&gt;There are too many ways to configure ESLint. I started with the &lt;code&gt;.eslintrcs.json&lt;/code&gt; config that came from &lt;a href="https://nextjs.org/docs/app/getting-started/installation" rel="noopener noreferrer"&gt;&lt;code&gt;create-next-app&lt;/code&gt;&lt;/a&gt;. However, this configuration &lt;a href="https://eslint.org/docs/latest/use/configure/configuration-files" rel="noopener noreferrer"&gt;is now deprecated&lt;/a&gt; in favor of &lt;code&gt;eslint.config.mjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because I couldn't easily extend this deprecated configuration, I (swore and then) scrapped it and initialized a new eslint config with &lt;code&gt;npm init @eslint/config@latest&lt;/code&gt;. I then added the configuration for &lt;code&gt;next&lt;/code&gt; wrapped with &lt;a href="https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config" rel="noopener noreferrer"&gt;eslint's flat compat utility&lt;/a&gt;. Here's the snippet in &lt;code&gt;eslint.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { FlatCompat } from "@eslint/eslintrc"
export default const config = [
// other ESLint plugins and rulesets
...compat.config({
    extends: ["next"],
    settings: {
      next: {
        rootDir: ".",
      },
    },
  }),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most other plugins offer a configuration that can be plugged into &lt;code&gt;eslint.config.mjs&lt;/code&gt; directly. But if you see an error starting with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Support for loading ES Module in require() is an experimental feature and might change at any time&lt;br&gt;
then you probably need to wrap a plugin with that &lt;a href="https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config" rel="noopener noreferrer"&gt;flat config migration utility&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Sharpen Skills by Exploring Linting Config
&lt;/h2&gt;

&lt;p&gt;Ultimately, there is no single "right" style guide or linting configuration. You should choose what works best for your project. When creating or revisiting my configurations, I often uncover new tricks or coding conventions that make me a better software engineer. when putting together this configuration, I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It's important to deal with the ambiguity of &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; in JS/TS, but &lt;em&gt;how&lt;/em&gt; you do it is project-dependent. By default, unicorn encourages use of &lt;code&gt;undefined&lt;/code&gt; over &lt;code&gt;null&lt;/code&gt; always, but there are very legitimate reasons to use &lt;code&gt;null&lt;/code&gt;, like if you have a lot of code that leverages an ORM with &lt;code&gt;null&lt;/code&gt; values. &lt;a href="https://github.com/sindresorhus/meta/discussions/7" rel="noopener noreferrer"&gt;This long thread&lt;/a&gt; has many great points and no single right answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid passing a function reference directly to iterators (i.e. &lt;code&gt;{elements.map(callback)}&lt;/code&gt;) &lt;a href="https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-callback-reference.md" rel="noopener noreferrer"&gt;details&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your style guide also shouldn't be static. It should evolve as your project grows. Not only do the needs of your project change, but conventions, libraries, and coding languages evolve around your work, too. It's okay (good, even) to make considered and reasoned changes to your conventions, even if you're backtracking on something you once felt was really important.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>eslint</category>
      <category>typescript</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>NextJS + Drizzle -- 8 Things I Learned Spinning up a New Project</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Fri, 01 Nov 2024 20:47:00 +0000</pubDate>
      <link>https://forem.com/jordanahaines/nextjs-drizzle-8-things-i-learned-spinning-up-a-new-project-53pd</link>
      <guid>https://forem.com/jordanahaines/nextjs-drizzle-8-things-i-learned-spinning-up-a-new-project-53pd</guid>
      <description>&lt;p&gt;The best way to learn is by building new stuff (for me, at least). I'm building &lt;a href="https://historio.app/" rel="noopener noreferrer"&gt;Historio&lt;/a&gt; -- a web app to explore history through timelines and your favorite books. I'm intentionally learning a new stack and dev tooling along the way. I'm a month or so in -- here's what I've learned creating a new NextJS + Drizzle project from scratch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learnbuildteach.substack.com/" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;Subscribe to Learn, Build, Teach {Repeat}!&lt;/a&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Run TS files as a script from your terminal
&lt;/h3&gt;

&lt;p&gt;To test and iterate on backend code, I've started calling some modules (like OpenAI researches) from scripts I execute via the terminal. Copious &lt;code&gt;console.debug&lt;/code&gt; and &lt;code&gt;console.warn&lt;/code&gt; statements make this a kinda effective way to debug without having to craft a frontend. To execute these scripts, I use &lt;a href="https://tsx.is/" rel="noopener noreferrer"&gt;TSX&lt;/a&gt; resulting in commands like this one that &lt;a href="https://github.com/jordanahaines/historio/blob/main/historio/lib/researchers/researchCoordinator.ts" rel="noopener noreferrer"&gt;extracts events from history books&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx tsx scripts/scriptProcessBook.ts&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. How &lt;em&gt;NOT&lt;/em&gt; to execute async functions in series
&lt;/h3&gt;

&lt;p&gt;Quiz: Will these async functions be executed in series or parallel?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_.range(10).forEach(async (idx) =&amp;gt; doSomething())&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The answer: In parallel, which is &lt;em&gt;not&lt;/em&gt; what I wanted. Turns out the right answer is a good ole' fashioned &lt;code&gt;for&lt;/code&gt; loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (let i = 0; i &amp;lt; 10; i++)
 doSomething()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. How to set common fields on all Drizzle models
&lt;/h3&gt;

&lt;p&gt;One pattern I love with Django’s ORM is using abstract models to define fields that are re-used across models. This makes it easy to ensure models are consistent and core fields are predictable. To achieve a similar pattern with drizzle, I defined common fields in an object that gets spread in model definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { timestamp, uuid } from "drizzle-orm/pg-core"

export const BASE_SCHEMA_FIELDS = {
  id: uuid("id").primaryKey().defaultRandom()
}

// to implement in a model:
import { BASE_SCHEMA_FIELDS } from "./common"

export const books = pgTable("books", {
  ...BASE_SCHEMA_FIELDS,
  title: varchar("title"),
  author: varchar("author"),
  // ... additional fields
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. How to deploy a static site with Cloudflare pages
&lt;/h3&gt;

&lt;p&gt;The way I deploy websites has evolved over time. My first sites were deployed by dragging a folder from my computer to a server with an FTP client; with git, I started using git pull on the server, followed by some bash scripts; the final evolution brought all of this into CI/CD pipelines that magically work most of the time and require &lt;em&gt;so much grief&lt;/em&gt; to debug the rest of the time.&lt;/p&gt;

&lt;p&gt;However, hose pipelines (even Github pages) are overkill if you merely want to deploy a static site.&lt;/p&gt;

&lt;p&gt;I'm trying (and mostly succeeding) not to over-engineer Historio, and stuck to Cloudflare pages to deploy the initial &lt;a href="https://historio.app/" rel="noopener noreferrer"&gt;landing page&lt;/a&gt;. It couldn't be easier. Literally &lt;a href="https://developers.cloudflare.com/pages/framework-guides/deploy-anything/" rel="noopener noreferrer"&gt;just drag and drop a zip folder&lt;/a&gt; with a static site and &lt;strong&gt;boom&lt;/strong&gt; it’s deployed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Rest!
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://learnbuildteach.substack.com/p/building-historio-episode-1-which" rel="noopener noreferrer"&gt;Read about my other 4 learnings here&lt;/a&gt;, including how to set &lt;code&gt;created&lt;/code&gt; and &lt;code&gt;updated&lt;/code&gt; timestamps on Drizzle models that automatically get set when objects are created/updated. If you find this useful, &lt;a href="https://learnbuildteach.substack.com/" rel="noopener noreferrer"&gt;subscribe to LBT{R}&lt;/a&gt; where I write about a range of web development and product management topics like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤔 For the Commments
&lt;/h3&gt;

&lt;p&gt;What were your first learnings with NextJS or Drizzle?&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>drizzle</category>
      <category>cloudflare</category>
      <category>react</category>
    </item>
    <item>
      <title>How to filter on number or date custom fields in ActionNetwork targeting</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Fri, 29 Mar 2024 19:45:00 +0000</pubDate>
      <link>https://forem.com/jordanahaines/how-to-filter-on-number-or-date-custom-fields-in-actionnetwork-targeting-36b8</link>
      <guid>https://forem.com/jordanahaines/how-to-filter-on-number-or-date-custom-fields-in-actionnetwork-targeting-36b8</guid>
      <description>&lt;p&gt;This week I learned how to filter (using less than/great than) on a number custom field in ActionNetwork targeting. Because we focus on young progressives at Run for Something, you can imagine we target folks based on their age, when available.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://help.actionnetwork.org/hc/en-us/articles/203113109-Creating-and-targeting-reports" rel="noopener noreferrer"&gt;ActionNetwork docs&lt;/a&gt; allude to using &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; to filter on custom number/date fields but this was not working for our &lt;code&gt;age&lt;/code&gt; custom field. The issue was that - in addition to ages - this field contained data like the string "n/a" and "prefer not to answer" (largely from old data sources that were not validated). So, to filter on a number or date field in ActionNetwork, as their docs suggest, you must &lt;strong&gt;ensure the custom field has a consistent data type&lt;/strong&gt; and that there aren't strings mixed in with date or number values (null values are fine).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feef7sk1v35p86f89a5mr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feef7sk1v35p86f89a5mr.png" alt="ActionNetwork Custom Field Filtering" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One way to ensure this is the case is to add some field validation. This is the closest analog to designating a field &lt;em&gt;type&lt;/em&gt; that ActionNetwork has.&lt;/p&gt;

</description>
      <category>todayilearned</category>
      <category>actionnetwork</category>
    </item>
    <item>
      <title>Work Hard, Play Hard(er)</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Mon, 02 Oct 2023 15:53:00 +0000</pubDate>
      <link>https://forem.com/jordanahaines/work-hard-play-harder-2e67</link>
      <guid>https://forem.com/jordanahaines/work-hard-play-harder-2e67</guid>
      <description>&lt;p&gt;&lt;a href="https://kalosal.com/work-hard-play-harder/" rel="noopener noreferrer"&gt;This post was originally published at Kalosal.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am somewhere in the middle of a journey from being an enthusiastic to skeptical participant in Grind Culture. &lt;a href="https://www.nytimes.com/2019/01/26/business/against-hustle-culture-rise-and-grind-tgim.html" rel="noopener noreferrer"&gt;This system measures worth and success based on the “grind” of work&lt;/a&gt;. Along the way, I've realized that the trick of relating work and play in the first place is deceptive. Hard work cannot be balanced by hard(er) leisure. And the grind comes from trying.&lt;/p&gt;

&lt;p&gt;How much is one hour of leisure worth to you? Your economics 101 professor may argue that it's worth exactly your wage because taking that hour of leisure means giving up the opportunity to work another hour. According to this logic, the average American's leisure time is valued at just under $30 per hour. &lt;/p&gt;

&lt;p&gt;Your professor - of course - is wrong. I doubt anyone making $55K per year would trade an hour of partying, attending a concert, or having dinner with their grandma for thirty bucks. (And yes, there are more complex economic theories that can assign higher values to an hour of leisure, but they all fall short in capturing the true value of leisure in the first place.)&lt;/p&gt;

&lt;p&gt;In America, especially in work environments affected by Grind Culture, the value of labor is not accurately reflected by the amount you're paid for it. For many people, there is an implicit expectation of future value associated with each hour of work. I like to call it the "American Dream". In this perspective, every hour you invest in your job is an investment in your future self – someone with enhanced skills, a stronger work ethic, and valuable professional connections. This is in addition to the 30 dollars you bring home.&lt;/p&gt;

&lt;p&gt;This concept of the American Dream may be most prescient for founders and small business owners who very literally have a vested financial interest in their work beyond their salary. For early-stage founders, a few extra hours of work now could potentially be worth millions of dollars in the future. Given the high stakes, it's hard to justify not working the extra hour or two.&lt;/p&gt;

&lt;p&gt;There is one thing your econ 101 professor wasn’t simplifying, though: Unless we’re careful, as the value of work inflates, the value of leisure rises to match it. More specifically, the value required &lt;strong&gt;of&lt;/strong&gt; leisure rises to match it. If working a few extra hours could potentially result in million-dollar gains, how can one justify taking a vacation or even spending an afternoon peacefully binge-watching a show on Netflix?&lt;/p&gt;

&lt;p&gt;By operating within this framework, you're essentially gambling with your free time. Sacrificing leisure for work that may or may not bring significant value is a risky bet. It may not pay off, but it will definitely deprive you of those “leisure” moments to relax, find joy, be present with loved ones, or grow as a person. Sound harsh? Perhaps this isn’t the right framework to consider  work and leisure in the first place.&lt;/p&gt;

&lt;p&gt;At one point, I thought I merely had the causality wrong. Maybe it's not that the value of work determines the price of leisure, but rather the value of leisure determines how much an hour of work is worth (or worth giving up). But even this perspective feels off. The value of leisure is different from the value of work, and both have real worth that cannot be quantified in dollars (regardless of how complex the economic formulae get).&lt;/p&gt;

&lt;p&gt;Grind Culture’s greatest trick is that it puts work and leisure in the same equation to begin with. As if they’re directly related. As if the value of leisure and work doesn’t change day to day depending on how you feel and what you need. As if doing more of one necessitates doing less of the other. We shouldn’t discuss work and leisure in the same sentence &lt;strong&gt;&lt;em&gt;except&lt;/em&gt;&lt;/strong&gt; to acknowledge that there’s leisure in work and work in leisure. Sometimes playing hard &lt;strong&gt;&lt;em&gt;is&lt;/em&gt;&lt;/strong&gt; hard work, and working hard &lt;strong&gt;is&lt;/strong&gt; a lot of fun. “Work/Life” balance is a false dichotomy that reads as if we’re balancing two things on a teeter totter; more of one, less of the other. “Work Hard Play Hard” goes a step further to imply that more work necessitates more leisure. Neither is true.&lt;/p&gt;

&lt;p&gt;The antidote? Work hard - or don’t. Play hard - or don’t. Balance hard work with easy work; un-fun work with fun work; play with leisure (they’re different); relaxation with growth. Be very wary of sacrificing “low-value” leisure for “high-value” work. Like doubling down on a bet when the odds are stacked against you, it’s a decision (and time) you can’t get back.&lt;/p&gt;

</description>
      <category>work</category>
      <category>productivity</category>
      <category>startup</category>
    </item>
    <item>
      <title>4 Bullet Thursday</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Fri, 13 Jan 2023 03:50:29 +0000</pubDate>
      <link>https://forem.com/jordanahaines/4-bullet-thursday-31ah</link>
      <guid>https://forem.com/jordanahaines/4-bullet-thursday-31ah</guid>
      <description>&lt;p&gt;&lt;em&gt;This post originally posted at &lt;a href="https://kalosal.com/4-bullet-thursday-1-12-23/" rel="noopener noreferrer"&gt;kalosal.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My past week, summed up in 4 bullet points on things I found cool or found myself returning to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I finally retired my &lt;a href="https://www.economist.com/weeklyedition/2022-12-24" rel="noopener noreferrer"&gt;Economist Holiday Double Issue&lt;/a&gt; this week. This issue - which spans the last two weeks of the year - is always my favorite because it combines two of my favorite things: the writing of The Economist and history. Some things I learned this year include: Chicago is the grid-iest city; if Satya Nadella and his friends have their way, Cricket may be the next big sport in the US; and guano (yes that guano) was a valuable commodity back in the day before synthetic nitrogen fertilizers. I nearly got through all the fun articles before the first issue of 2023 arrived. (I have a strict rule that the previous issue of any periodical must be recycled when the next issue arrives lest they pile up and create the hell described in The Good Place).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This week I was playing &lt;a href="https://www.nintendo.com/store/products/factorio-switch/" rel="noopener noreferrer"&gt;Factorio On Switch&lt;/a&gt;. I'm one of those people who likes my games to feel like work. Because of the cult popularity of Factorio, I know I'm not alone. Factorio basically boils down to a huge optimization problem as you configure robots and complex manufacturing workflows to research and build a series of successively more resource-consuming items. Factorio is incredibly well-balanced and challenging enough to keep me returning for casual play. If you like simulation/management games, check it out!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We got a &lt;a href="https://www.breville.com/us/en/products/espresso/bes450.html" rel="noopener noreferrer"&gt;Breville Bambino&lt;/a&gt; as a wedding gift and now that I've started using it I literally can't stop. With no shame, I'm buying vanilla syrup by the case and making myself lattes for dayyys. I also have so much more respect for baristas after the stress of deciding to make a latte 4 minutes before a meeting starts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every couple of months I totally revamp my task, notes, and planning systems. There is perhaps no better catalyst for this change than New Years Resolutions (which of course must be specific, measurable, attainable, relevant, and timely). My most recent revamping involved moving all of my tasks - including goals and habits - into ClickUp (which I was previously using just for work). Before you start thinking that these posts are all about me flogging products, let me be very clear that I don't recommend ClickUp (notice I didn't link it. That'll show 'em). It's buggy and clunky and better for teams than individuals. BUT at least all of my stuff is in one place now. And I've found that when it comes to task systems, it's constantly revamping them that matters far more than what system one uses at a given time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reflection</category>
      <category>economist</category>
      <category>espresso</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Product Development Cycle</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Sun, 02 Jan 2022 14:56:49 +0000</pubDate>
      <link>https://forem.com/kalosal/the-product-development-cycle-4j1m</link>
      <guid>https://forem.com/kalosal/the-product-development-cycle-4j1m</guid>
      <description>&lt;p&gt;Originally posted on &lt;a href="https://codifiedmusings.com/product-development-cycle" rel="noopener noreferrer"&gt;Codified Musings&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Delivering great product to customers is hard. Commonly, the product and design team thinks the software engineers cut the one feature that ties the product together because it “literally like quadruples our complexity”. The engineering team thinks the product is poorly specified. The operations team thinks the solution is over-engineered and difficult to roll out. And the leadership team just wants to know why the whole process is taking so darn long.&lt;/p&gt;

&lt;p&gt;Great product management serves to placate these varied (and legitimate) concerns while still delivering a delightful product to end-users that is sorta on time and maybe even within budget.&lt;/p&gt;

&lt;p&gt;As I tease out what makes great product management…great… in other posts, it’s worth taking a moment to explore the &lt;strong&gt;product development lifecycle (PDL)&lt;/strong&gt;. In this post I will outline the 5 stages of the product development lifecycle and describe why the lifecycle as a whole is useful. In later posts I’ll dive deeper into each stage to discuss common pitfalls, what success &lt;em&gt;really&lt;/em&gt; looks like, and how to best engage product stakeholders.&lt;/p&gt;

&lt;p&gt;First: What the heck is this PDL thing?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Development Lifecycle
&lt;/h2&gt;

&lt;p&gt;In a nutshell, the product development lifecycle includes everything that happens between when it becomes apparent a new feature is necessary to it being rolled out to customers and even iterated upon. Crucially, ideation is a part of the product development lifecycle, so the process often begins before what becomes the product even crosses anyone’s mind. The PDL can be broken down into 5 stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the problem&lt;/li&gt;
&lt;li&gt;Design a solution&lt;/li&gt;
&lt;li&gt;Build and test the solution&lt;/li&gt;
&lt;li&gt;Rollout!&lt;/li&gt;
&lt;li&gt;Measure success and Iterate&lt;/li&gt;
&lt;/ol&gt;



&lt;h2&gt;
  
  
  1. Identify the Problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools for this stage:&lt;/strong&gt; &lt;a href="https://www.mindtools.com/pages/article/newCT_02.htm" rel="noopener noreferrer"&gt;SCAMPER&lt;/a&gt;, &lt;a href="https://uxdesign.cc/how-to-create-rock-solid-product-design-hypotheses-a-step-by-step-guide-e2443d421f21" rel="noopener noreferrer"&gt;Product Hypothesis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great product starts with a real problem. Unsuccessful products are often great solutions in search of a problem.  In this phase you’ll first identify the problem to be solved and then ideate solutions. Note that another way to think of the “problem” to be identified is actually as an opportunity: New products capitalize on the &lt;em&gt;opportunity&lt;/em&gt; to solve a problem for users. &lt;/p&gt;

&lt;p&gt;Be wary of products that start as a solution (“Wouldn’t it be cool if we made a mobile app version of our website”) as oppose to a problem (“I noticed that most of our users abandon our site during registration, and most of our competitors have mobile apps that don’t require registration. Should we build a mobile app?”).&lt;/p&gt;

&lt;p&gt;To rock problem identification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure that the problem being solved is visible in data. For example, if the problem you are trying to solve is that people never return to your site after registering for an account, you should have clear abandonment and retention metrics that illustrate this problem. Identifying these metrics at the beginning is also important because their improvement is how you’ll ultimately measure success. Be wary of attempting to solve a problem that is not yet measured.&lt;/li&gt;
&lt;li&gt;Identify the stakeholders whose perspective you’ll need to design the perfect solution to the problem. This should include end-users, and may also include other product teams, company leadership, support, sales, and even marketing teams.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;At the end of this stage you will have:&lt;/strong&gt; A single clearly identified problem to be solved visible in data, including an idea of the KPIs that can be used to confirm the problem is solved.&lt;/p&gt;



&lt;h2&gt;
  
  
  2. Design a Solution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; &lt;a href="https://www.swotanalysis.com/blog/swot-analysis-on-a-product-or-service" rel="noopener noreferrer"&gt;SWOT&lt;/a&gt; and &lt;a href="https://www.intercom.com/blog/rice-simple-prioritization-for-product-managers/" rel="noopener noreferrer"&gt;RICE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let the brainstorming begin! Ideate the solutions to the problem you’ve identified. In this stage, you should pay special attention to your &lt;a href="https://xd.adobe.com/ideas/process/user-research/putting-personas-to-work-in-ux-design/#:~:text=User%20personas%20are%20archetypical%20users,see%20in%20the%20example%20below" rel="noopener noreferrer"&gt;user personae&lt;/a&gt;.) (you have those defined, right?). This is especially important if multiple types of users face the problem you’ve outlined. Your solution should be a &lt;a href="https://www.investopedia.com/terms/p/paretoimprovement.asp" rel="noopener noreferrer"&gt;Pareto improvement&lt;/a&gt;: benefiting at least 1 type of user while not making things &lt;em&gt;worse&lt;/em&gt; for any other persona. If you end up needing multiple solutions for your different personae, that’s okay!&lt;/p&gt;

&lt;p&gt;Remember those 5th grade science fair projects? Turns out that just like in those projects, &lt;a href="https://uxdesign.cc/how-to-create-rock-solid-product-design-hypotheses-a-step-by-step-guide-e2443d421f21" rel="noopener noreferrer"&gt;thinking of new product in the form of a hypothesis&lt;/a&gt; is super useful. Phrase each of your potential solutions in the form of a product hypothesis like the one below. This construct ensures you have a clearly identified problem, and that the proposed solution is a (potential) way to solve it. Your product hypotheses should take the form: &lt;em&gt;By doing , we will  as measured by .&lt;/em&gt; Example product hypothesis:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By automatically sending engagement texts from customer success team members, we can increase customer retention in the first week after account creation as measured by our daily customer retention KPI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Need help choosing the right solution? If you’re stuck, consider a framework like &lt;a href="https://www.intercom.com/blog/rice-simple-prioritization-for-product-managers/" rel="noopener noreferrer"&gt;RICE&lt;/a&gt; or &lt;a href="https://www.swotanalysis.com/blog/swot-analysis-on-a-product-or-service" rel="noopener noreferrer"&gt;SWOT&lt;/a&gt; to evaluate and compare solutions. Ensure you have a solid product hypothesis for your final idea and then craft a Product Requirements Document (PRD) that articulates the goals, granular requirements, and implementation details for the product.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;At the end of this stage you will have:&lt;/strong&gt; A single product idea that you’ve decided to move forward with, and a few other candidates you considered. The final product idea has a clear product hypothesis and measurable goal, is specified through PRD/TSD, and has buy-in from all key stakeholders (including the end-users you - as PM - represent).&lt;/p&gt;



&lt;h2&gt;
  
  
  3. Build and Test the Solution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; &lt;a href="https://asana.com/resources/critical-path-method" rel="noopener noreferrer"&gt;Critical Path Method&lt;/a&gt; — &lt;a href="https://shortcut.com/" rel="noopener noreferrer"&gt;Shortcut&lt;/a&gt;; &lt;a href="https://www.atlassian.com/software/jira" rel="noopener noreferrer"&gt;Jira&lt;/a&gt; — &lt;a href="https://www.productplan.com/glossary/product-requirements-document/" rel="noopener noreferrer"&gt;PRD&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bad product teams spend all of their time on this stage. Great product teams realize that investing in the other four stages of the PLD makes this stage - building the solution - easier. So don’t start with this stage; don’t end with this stage. And if you find yourself dreading this stage, then you probably aren’t spending enough energy on the others (hint: Stage 2 is where to look first!).&lt;/p&gt;

&lt;p&gt;In this stage you work closely with your design and engineering teams to bring the designed solution to fruition. Unless you’re directly writing code, your role during this stage verges into that of a project manager: keep the feature development on schedule.&lt;/p&gt;

&lt;p&gt;You should be anticipating the challenges that will arise and circumventing them as best you can. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensuring any questions on the PRD receive swift and thorough answers.&lt;/li&gt;
&lt;li&gt;Ensuring all copy, messaging, and other “soft” parts of the specification are included before engineers need them.&lt;/li&gt;
&lt;li&gt;Providing swift feedback on features as they are completed so the engineering team can focus on building and iterating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll likely use some technical project management software like &lt;a href="https://shortcut.com/" rel="noopener noreferrer"&gt;Shortcut&lt;/a&gt; or &lt;a href="https://www.atlassian.com/software/jira" rel="noopener noreferrer"&gt;Jira&lt;/a&gt; during this stage to track the progress of features as they are built. These software provide tools like burn down charts and other velocity metrics that can help you see what components are on time or falling behind.&lt;/p&gt;

&lt;p&gt;Don’t forget to test! I’m sure your engineering team only builds perfect bug-free products 😏. But even if they do, it’s important to test to ensure the product works as intended and actually achieves the vision set out in the product hypothesis. Get feedback from as many stakeholders as possible, including real end-users where you can (if you can’t, then your job is to represent them as you test yourself). Testing is complete when all stakeholders agree that the product is ready to be rolled out to newly delighted end-users.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;At the end of this stage you will have:&lt;/strong&gt; A built and tested prototype that is ready to roll out to at least some of your users.&lt;/p&gt;



&lt;h2&gt;
  
  
  4. Rollout
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt;: Internal Knowledge Base like &lt;a href="https://www.notion.so/" rel="noopener noreferrer"&gt;Notion&lt;/a&gt;, &lt;a href="https://www.atlassian.com/software/confluence" rel="noopener noreferrer"&gt;Confluence&lt;/a&gt; or &lt;a href="https://www.getguru.com/" rel="noopener noreferrer"&gt;Guru&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The software is built! Time to deploy and then you’re done, right? Almost. Rolling out complex software requires a lot of coordination. As PM, it is your responsibility to ensure this coordination happens, that everyone knows that the new product &lt;em&gt;exists&lt;/em&gt; as well as how to use it, and that reference materials exist for new customers/team members (or those that are apt to forget).&lt;/p&gt;

&lt;p&gt;Ensure that your product update is well documented prior to launch. Expect people on your team and customers to ask some variation of “what the heck is this?” Instead of copying and pasting the same response over and over again, do yourself a favor and curate documentation ahead of time in a knowledge base like &lt;a href="https://www.notion.so/" rel="noopener noreferrer"&gt;Notion&lt;/a&gt;, &lt;a href="https://www.atlassian.com/software/confluence" rel="noopener noreferrer"&gt;Confluence&lt;/a&gt; or &lt;a href="https://www.getguru.com/" rel="noopener noreferrer"&gt;Guru&lt;/a&gt; that you can link to. Particularly when the product changes internal operations, you may need to create separate public-facing documentation (for customers) and internal documentation (for ops/support teams).&lt;/p&gt;

&lt;p&gt;PMs who were previously software engineers tend to underestimate the importance (and work required) of this stage. Engineers are used to “finishing” a product once the code is written and feature is tested. But the PM’s job is not done until product is built, actually used, and achieving success as laid out in your product hypothesis.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;At the end of this stage you will have:&lt;/strong&gt; Documentation for your team and end-users describing your new product and how it works. Support and operations teams will understand the new product in-depth, including any new tools at their disposal to support customers using the product.&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Measure Success and Iterate
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; BI or KPI dashboards like &lt;a href="https://datastudio.google.com/u/0/" rel="noopener noreferrer"&gt;Datastudio&lt;/a&gt; or &lt;a href="https://looker.com/" rel="noopener noreferrer"&gt;Looker&lt;/a&gt; — Product Hypothesis (round2)&lt;/p&gt;

&lt;p&gt;Even once your great new product is in the hands of end-users, your job is not done. Return to your product hypothesis from stage 2: What was the goal of this product again? How were you going to measure success?&lt;/p&gt;

&lt;p&gt;It’s time to see your work pay off: Measure the KPIs you identified from stage 2 and compare them to where they were &lt;em&gt;before&lt;/em&gt; your new product went live. An improvement in those indicators signifies a product success. Of course, the usual caveats of not confusing correlation with causation apply; if there were &lt;em&gt;other&lt;/em&gt; initiatives that coincided with the launch of your product and could have affected your KPIs then you’ll need to adjust your assessment accordingly. If you’re really geeky, you can even test for a statistically significant improvement.&lt;/p&gt;

&lt;p&gt;KPIs didn’t improve? Well that’s kind of a win, too. You found something that didn’t work, or at least didn’t work in the way it was built. This is where the “cycle” part of the Product Development Cycle comes into play. Circle back to those KPIs that have yet to be improved, and return to Stage 1 to define the problem and (in Stage 2) ideate solutions to build upon your success.&lt;/p&gt;

&lt;p&gt;But be careful! One common PM pitfall is focusing only on product that isn’t working well. While this is important, don’t neglect further improving the parts of your product that are successful to make them even more so (more on this trade off later 😉).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;At the end of this stage you will have:&lt;/strong&gt; Measured the success of your new product against your product hypothesis and KPIs. Areas of further improvement - both in product that did and did not achieve its goal - are identified and you’re eager to jump into another go around the Product Development Cycle.&lt;/p&gt;

</description>
      <category>product</category>
      <category>management</category>
      <category>pm</category>
    </item>
    <item>
      <title>Django and Cloudwatch: Logging in a place you can see</title>
      <dc:creator>Jordan Haines</dc:creator>
      <pubDate>Sun, 24 May 2020 17:41:24 +0000</pubDate>
      <link>https://forem.com/jordanahaines/django-and-cloudwatch-logging-in-a-place-you-can-see-3bc7</link>
      <guid>https://forem.com/jordanahaines/django-and-cloudwatch-logging-in-a-place-you-can-see-3bc7</guid>
      <description>&lt;p&gt;This post describes how to get Django log messages into Cloudwatch. We'll configure a Cloudwatch log handler, and then outfit a DRF view to log a message to our Cloudwatch stream when a new object is created. This mimics a recent practical use case I encountered: Logging every request to a public API endpoint in Cloudwatch to aid in debugging. Let's get into it!&lt;/p&gt;

&lt;p&gt;Originally posted on &lt;a href="https://codifiedmusings.com/django-cloudwatch" rel="noopener noreferrer"&gt;Codified Musings&lt;/a&gt;, cross posted on &lt;a href="https://tldlife.com/get-your-django-logs-into-cloudwatch-1d0911f55b8f" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/jordanahaines/public-projects-and-wiki/-/tree/blog_posts/cloudwatch/projects/django-starter-project" rel="noopener noreferrer"&gt;&lt;strong&gt;Complete Demo Code&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup Cloudwatch - Copy AWS credentials
&lt;/h3&gt;

&lt;p&gt;This post will not go into detail on setting up an AWS account. From AWS, you'll need the Access Key and ID for a user with write access to Cloudwatch logs. I'd recommend &lt;em&gt;not&lt;/em&gt; using account you use to login to AWS. Instead, create an IAM user who we can grant Cloudwatch write access to. I tend to create separate users for dev, staging, and (especially) production environments for security and to aid in tracking down issues. At the end of the day, you'll need to make sure the IAM user you're using has the &lt;em&gt;CloudWatchLogsFullAccess&lt;/em&gt; permission, and you'll want to copy both the &lt;em&gt;Access Key ID&lt;/em&gt; and &lt;em&gt;Access Key Secret&lt;/em&gt; for an access key for your user (click Create access key if you need new credentials)&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Django Logging
&lt;/h3&gt;

&lt;p&gt;We are going to override the &lt;a href="https://docs.djangoproject.com/en/3.0/topics/logging/#configuring-logging" rel="noopener noreferrer"&gt;Django Logging setting&lt;/a&gt; to create a new logger, handler, and formatter to get our log messages into Cloudwatch.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install the python packages we need (&lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html" rel="noopener noreferrer"&gt;boto3&lt;/a&gt; to interact with the AWS API, and &lt;a href="https://pypi.org/project/watchtower/" rel="noopener noreferrer"&gt;Watchtower&lt;/a&gt; to get push logs to Watchtower, specifically). Run &lt;code&gt;pip install watchtower boto3&lt;/code&gt; (you're running in a &lt;a href="https://tutorial.djangogirls.org/en/django_installation/" rel="noopener noreferrer"&gt;virtual environment&lt;/a&gt;, right?).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In your settings file, instantiate a Boto3 session that Watchtower can use to connect to your AWS Cloudwatch account. In this example, I've put my AWS access key ID and Secret in the &lt;code&gt;AWS_ID&lt;/code&gt; and &lt;code&gt;AWS_KEY&lt;/code&gt; environmental variables.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;boto3.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;
&lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;span class="n"&gt;CLOUDWATCH_AWS_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AWS_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CLOUDWATCH_AWS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AWS_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AWS_DEFAULT_REGION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;# Be sure to update with your AWS region
&lt;/span&gt;&lt;span class="n"&gt;logger_boto3_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CLOUDWATCH_AWS_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CLOUDWATCH_AWS_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Define new &lt;code&gt;LOGGING&lt;/code&gt; settings in your Django settings file. Here's the complete &lt;code&gt;LOGGING&lt;/code&gt; settings you'll want to add (&lt;a href="https://gitlab.com/jordanahaines/public-projects-and-wiki/-/blob/blog_posts/cloudwatch/projects/django-starter-project/demoschool/settings.py" rel="noopener noreferrer"&gt;example&lt;/a&gt;):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LOGGING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disable_existing_loggers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s [%(levelname)-8s] %(message)s [%(pathname)s:%(lineno)d]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datefmt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handlers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower.CloudWatchLogHandler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;# From step 2
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boto3_session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logger_boto3_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log_group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DemoLogs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;# Different stream for each environment
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;console&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logging.StreamHandler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loggers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Use this logger to send data just to Cloudwatch
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handlers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propogate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s [%(levelname)-8s] %(message)s [%(pathname)s:%(lineno)d]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datefmt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds a new &lt;em&gt;formatter&lt;/em&gt; which defines how our logged messages will appear in Cloudwatch. This formatter ensures the time and log level are in the title. Here is what the resulting display looks like in Cloudwatch:&lt;br&gt;
&lt;code&gt;2020-05-24 15:26:48 [INFO ] Student Created: Arya Start (PK: 4) [.../django-starter-project/schoolusers/views.py:23]&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"handlers": {...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section creates a new handler that will forward all logs of level &lt;code&gt;INFO&lt;/code&gt; or higher, to the designated &lt;code&gt;loggers&lt;/code&gt; (in this case, just our &lt;code&gt;watchtower&lt;/code&gt; logger). The &lt;code&gt;log_group&lt;/code&gt; key indicates the log group, and &lt;code&gt;stream_name&lt;/code&gt; the Cloudwatch stream within that group, that our message will appear under. Finally, we specify the &lt;code&gt;aws&lt;/code&gt; formatter which dictates how logged messages will appear&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loggers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Use this logger to send data just to Cloudwatch
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handlers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propogate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section defines a new logger for Cloudwatch. All messages of &lt;code&gt;level&lt;/code&gt; or higher (that is, level "INFO") or higher, will be sent to the &lt;code&gt;watchtower&lt;/code&gt; handler. The &lt;code&gt;propogate&lt;/code&gt; property determines whether messages will be sent to additional loggers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Posting a Log Message From a View
&lt;/h3&gt;

&lt;p&gt;Alright, we've configured our Cloudwatch logger. Let's test it out. I added the following two lines to one of my views, specifically when a student is created in my school demo project (&lt;a href="https://gitlab.com/jordanahaines/public-projects-and-wiki/-/blob/blog_posts/cloudwatch/projects/django-starter-project/schoolusers/views.py" rel="noopener noreferrer"&gt;complete views code&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watchtower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Student Created: &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;student&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (PK: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;student&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then test by posting to this view with &lt;a href="https://insomnia.rest/" rel="noopener noreferrer"&gt;Insomnia Rest Client&lt;/a&gt;. For this post, I also include &lt;a href="https://gitlab.com/jordanahaines/public-projects-and-wiki/-/blob/blog_posts/cloudwatch/projects/django-starter-project/schoolusers/tests/scripts.py" rel="noopener noreferrer"&gt;example code&lt;/a&gt; that executes a request that uses this view, and thus should post a log message to Cloudwatch if you've configured your logger properly.&lt;/p&gt;

&lt;p&gt;In my demo project, when a POST is made to create a new student, I see a message like the following appear within Cloudwatch 😀:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fwvtkfqhc725cf07ztbuq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fwvtkfqhc725cf07ztbuq.png" alt="Cloudwatch log message example" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enriching your log messages
&lt;/h3&gt;

&lt;p&gt;I lied - the example above is not the extent of the logging I do with Cloudwatch. Because we have apps running in separate environments for local development, staging, testing, and product, we break out the logs for each of these environments to make messages easier to find (and to potentially add alarms to the production logs). In case this is also useful to you, we do this by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Defining an &lt;code&gt;ENV&lt;/code&gt; setting that describes what environment we're in. This setting can be set from an environmental variable, or you can use a different settings file for each environment to set a different value. In our logging configuration, we include &lt;code&gt;ENV&lt;/code&gt; in the &lt;code&gt;stream_name&lt;/code&gt; so that each environment has its own log stream within our Cloudwatch log group.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We define an aws logging &lt;code&gt;filter&lt;/code&gt; that attaches &lt;code&gt;ENV&lt;/code&gt; to a new &lt;code&gt;env&lt;/code&gt; property on the message. This was a key learning for me -- &lt;strong&gt;you can use a filter to add data to a log message&lt;/strong&gt;*:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CloudwatchLoggingFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Filter which injects context for env on record &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ENV&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="c1"&gt;# All messages make it through filter
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;We use this new &lt;code&gt;env&lt;/code&gt; property in the title of the message for good measure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the code you'll find in the &lt;a href="https://gitlab.com/jordanahaines/public-projects-and-wiki/-/tree/blog_posts/cloudwatch/projects/django-starter-project" rel="noopener noreferrer"&gt;demo code&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An aside on Django Logging
&lt;/h2&gt;

&lt;p&gt;As I dove into this topic, I found Django logging to be quite a bit more complex than expected. The &lt;a href="https://docs.djangoproject.com/en/3.0/topics/logging/#configuring-logging" rel="noopener noreferrer"&gt;Django Logging Documentation&lt;/a&gt; does a great job of explaining how logging works, how to setup your own loggers, and how to get different types of log messages to different places. Below are my notes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview&lt;/strong&gt;:&lt;br&gt;
&lt;strong&gt;Loggers&lt;/strong&gt; are buckets where messages (&lt;strong&gt;Log Records&lt;/strong&gt;) are sent. Loggers have a Log Level (DEBUG, INFO, WARNING, ERROR, CRITICAL); Loggers ignore records with a log level below the logger's level.&lt;/p&gt;

&lt;p&gt;Loggers send messages (that they don't ignore) to one or more &lt;strong&gt;Handlers&lt;/strong&gt;. Handlers determine what to do with a log record - like writing to console, a file or - as in our example above - to a log repository like CloudWatch. There is a many-to-many relationship between handlers and loggers, so the same handler can be re-used across many loggers. Handlers have their own log level, and ignore records below the handler's log level (allowing different forms of notification - based on record level - for the same logger).&lt;/p&gt;

&lt;p&gt;I know what you're thinking: This whole system is not nearly granular enough. Enter filters. &lt;strong&gt;Filters&lt;/strong&gt; provide additional control over which log records make their way from a logger to a handler. Filters can alter the log level of a log record; they can also suppress log records based on their source or other metadata on the log record. Filters can be chained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Big Revelation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Filters can attach additional information to log records&lt;/strong&gt; which can then be used in formatters to customize the display. I used this feature to attach a Django setting variable (&lt;code&gt;ENV&lt;/code&gt;) to records, so the &lt;code&gt;ENV&lt;/code&gt; is visible in the log record in CloudWatch.&lt;/p&gt;

&lt;p&gt;Finally, once a record makes its way to a handler and the record has a log level at or above that of the handler a &lt;strong&gt;formatter&lt;/strong&gt; is used to determine how the record is written by the handler. A formatter is just a python formatting string that leverages the &lt;a href="https://docs.python.org/3/library/logging.html#logrecord-attributes" rel="noopener noreferrer"&gt;Log Record Attributes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cool? Cool. In summary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log Record created. Sent to all loggers, but &lt;em&gt;ignored&lt;/em&gt; by loggers w/Log Level above that of the log record.&lt;/li&gt;
&lt;li&gt;Filters (associated with each logger) are applied to determine which records actually get passed to handler(s).&lt;/li&gt;
&lt;li&gt;Records that aren't suppressed by filters get sent to all of the handlers on logger. Handlers ignore records below the handler's log level&lt;/li&gt;
&lt;li&gt;Handler uses formatter to determine what the record will look like when you read it. Handler writes the record (to a file, or console, or Cloudwatch, etc).&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>django</category>
      <category>cloudwatch</category>
      <category>logging</category>
    </item>
  </channel>
</rss>
