Skip to content

Blog

Globalize Dates and Times in Django

From Flatter-Than-a-Pancake Kansas

Django's default date and time formats, from django.conf.locale.en.formats, reflect its Lawrence, Kansas roots:

Name Django Format String Sample Comments
DATE_FORMAT N j, Y April 29, 2026 Unambiguous, no time zone, months are language-specific, width varies
SHORT_DATE_FORMAT m/d/Y 04/29/2026 Ambiguous, no time zone
TIME_FORMAT P 2:30 p.m. Unambiguous, no time zone, slightly verbose, width varies
DATETIME_FORMAT N j, Y, P April 29, 2026, 2:30 p.m. Combine above
SHORT_DATETIME_FORMAT m/d/Y P 04/29/2026 2:30 p.m. Combine above

Around the Whole Globe

ISO 8601

The target is RFC 3339, a profile of ISO 8601 that uses a space between date and time instead of the T separator. The space is easier to read: 2026-04-29 14:30 Z vs 2026-04-29T14:30:00Z. With globalized settings:

Name Django Format String Sample
DATE_FORMAT Y-m-d \Z 2026-04-29 Z
SHORT_DATE_FORMAT Y-m-d \Z 2026-04-29 Z
TIME_FORMAT H:i \Z 14:30 Z
DATETIME_FORMAT Y-m-d H:i \Z 2026-04-29 14:30 Z
SHORT_DATETIME_FORMAT Y-m-d H:i \Z 2026-04-29 14:30 Z

Putting a timezone on a bare date is basically unheard of. But consider two people looking at the same date string:

Viewer Offset Their interpretation UTC span
Reader in California (PDT) UTC-7 Apr 29, midnight to midnight Pacific 2026-04-29T07:00Z to 2026-04-30T06:59Z
Writer in India (IST) UTC+5:30 Apr 29, midnight to midnight IST 2026-04-28T18:30Z to 2026-04-29T18:29Z

Their interpretations differ by 12 hours 30 minutes. In the worst case--a UTC-12 reader and a UTC+14 writer--the same date spans a 50-hour ambiguity window.

Writing America/Los_Angeles or EST5EDT next to every date would be unwieldy. But Z--two characters including the space--tells the reader this date is UTC, narrowing the ambiguity window from 50 hours to 24. For internal systems that already run on UTC, the burden is zero.

This is globalization: one format, everywhere, no locale adaptation.

Django's USE_I18N setting controls string translation, not date formatting. The setting that controls whether formats adapt to the user's locale was historically USE_L10N, for "localization." Django 5.0 removed USE_L10N. The replacement is FORMAT_MODULE_PATH.

Here is how to configure RFC 3339 datetimes across Django versions.

The goal

Display datetimes like 2026-04-29 14:30 Z everywhere:

  • 24-hour time
  • Explicit Z timezone suffix
  • No locale adaptation -- every user, every page, every format

Django 3.x and 4.x

Set USE_L10N = False. Django then uses the format strings from config/settings.py directly, without consulting locale modules. USE_L10N is deprecated in 4.x but still works.

1
2
3
4
5
6
7
8
9
# config/settings.py
USE_I18N = False
USE_L10N = False
USE_TZ = True
TIME_ZONE = 'UTC'

DATETIME_FORMAT = SHORT_DATETIME_FORMAT = '%Y-%m-%d %H:%M Z'
DATE_FORMAT = SHORT_DATE_FORMAT = '%Y-%m-%d Z'
TIME_FORMAT = '%H:%M Z'

These are Python strftime format strings. The Z is a literal character.

Django 5.x and 6.x

USE_L10N is removed. The settings-level DATETIME_FORMAT strings are ignored because Django's built-in locale formats take priority -- even with USE_I18N = False.

Django resolves format strings by checking modules in FORMAT_MODULE_PATH before falling back to the built-in django.conf.locale.en.formats. We register our own module to override the defaults.

Create a format module at config/formats/z/formats.py:

1
2
3
4
5
6
# config/formats/z/formats.py
DATETIME_FORMAT = r'Y-m-d H:i \Z'
SHORT_DATETIME_FORMAT = r'Y-m-d H:i \Z'
DATE_FORMAT = r'Y-m-d \Z'
SHORT_DATE_FORMAT = r'Y-m-d \Z'
TIME_FORMAT = r'H:i \Z'

Format modules use Django's own template-tag format syntax, not Python strftime. The equivalent of %Y-%m-%d %H:%M Z is Y-m-d H:i \Z (backslash-escaped Z literal).

Point Django at the module:

1
2
3
4
5
# config/settings.py
USE_I18N = False
USE_TZ = True
TIME_ZONE = 'UTC'
FORMAT_MODULE_PATH = 'config.formats'

How Django resolves formats

When code calls formats.get_format('DATETIME_FORMAT'), Django follows this path:

  1. If locale formatting is enabled, check each module returned by get_format_modules()
  2. Modules are searched in order: FORMAT_MODULE_PATH first, then built-in locale modules
  3. If a module defines the requested format, use that value
  4. Otherwise fall back to the value from config/settings.py

With FORMAT_MODULE_PATH = 'config.formats', our module is checked before django.conf.locale.en.formats, so our RFC 3339 format wins.

What about USE_I18N?

USE_I18N controls Django's translation machinery of gettext, {% translate %}, language-prefixed URLs, and so on. It does not control date formatting. You can set USE_I18N = True (to translate strings) while still using RFC 3339 datetimes via FORMAT_MODULE_PATH. The two are independent.

Setting USE_I18N = False is still useful: it avoids loading translation catalogs at startup and prevents Django from looking for locale-specific format modules that do not exist. It also shuts off the admin's language-preference UI.

See also

Authenticate git with gh over HTTPS

gh auth login does more than GitHub API access; it also configures git's credential helper. From then on, HTTPS remotes authenticate as whichever account is active in gh.

1
2
3
4
5
6
# New repo: clone over HTTPS, not SSH
gh repo clone biobuddies/helicopyter
# remote is https://github.com/biobuddies/helicopyter.git

# Existing repo: switch to HTTPS
git remote set-url origin https://github.com/biobuddies/helicopyter.git

Behind the scenes, gh registers itself as the credential helper:

1
2
3
# git config --list | grep credential.https.github
credential.https://github.com.helper=
credential.https://github.com.helper=!/path/to/gh auth git-credential

When git needs credentials, it calls gh auth git-credential, which reads the stored token.

Token storage depends on the platform:

  • macOS: osxkeychain, the Keychain Access app
  • Linux: ~/.config/gh/hosts.yml (encrypted with a key from pass, gpg, or plain file)

gh auth status confirms which account is active:

1
2
3
4
5
# gh auth status
github.com
  ✓ Logged in to github.com account covingtron (keyring)
  - Active account: true
  - Git operations protocol: https

No SSH keys needed, no ~/.ssh/config. Every push authenticates as the active gh account.

In GitHub Actions, GITHUB_TOKEN is available automatically via OIDC. When $GITHUB_TOKEN is set, gh auth git-credential uses it instead of a stored token; no gh auth login needed in CI. This is the same token that actions/checkout uses for HTTPS clones.

Pip and uv support git+https:// dependencies. There are two ways to authenticate them:

1
2
3
4
5
# Token in URL; pip/uv parse it directly, no git credential helper needed
hadolint-py @ git+https://${GITHUB_TOKEN}@github.com/AleksaC/hadolint-py.git

# Clean URL; git resolves creds via gh auth git-credential
hadolint-py @ git+https://github.com/AleksaC/hadolint-py.git

Token-in-URL works everywhere but risks leaking into lock files; older versions of uv and pip-compile could bake the token into requirements.txt. With gh auth git-credential, the URL stays concise and the credential helper handles auth transparently; git, pip, and uv all use it. This requires gh installed in CI.

Favorite Mountain Singalongs

Two mountain songs work well with the same three open chords. This version keeps both in G, so a group can stay on G, C, and D7 all the way through.

Try ↓ ↓↑ ↑↓↑ all the way through, or just keep a steady down-strum if the room is loud.

The Bear Went Over the Mountain

The bear went over themoun-tain

The bear went over themoun-tain

The bear went over themoun-tain

To seewhat hecouldsee

And allthat hecouldsee

And allthat hecouldsee

And allthat hecouldsee

Was theother sideof themoun-tain

She'll Be Coming 'Round the Mountain

She'll be coming'round themoun-tainwhen shecomes

She'll be coming'round themoun-tainwhen shecomes

She'll be coming'round themoun-tain

She'll be coming'round themoun-tain

She'll be coming'round themoun-tainwhen shecomes

If you want a little more motion, hold D7 on the last cell, then turn back to G as the next line starts.

Christ the Lord Is Risen Today

The last post walked through ASCII art, CSS grids, sparse position markup, and the Unicode fretboard glyph. For a full seasonal song, this version uses the glyph-and-position approach with build-time templating, so concise source expands into a somewhat verbose HTML page.

In G:

Try one of these 4/4 strumming patterns:

  • ↓ ↓↑ ↑↓↑
  • ↓ ↓ ↓↑ ↓↑

Christ the Lord isrisen to day-ay, a-al-le-lu-u-ia!

Sons of men andangelssay-ay, a-al-le-lu-u-ia!

Raise your joys andtry-umphs,high,a-al-le-lu-u-ia!

Sing, ye heavens, andearth re-ply-y, a-al-le-lu-u-ia!

Lives again ourglorious Ki-ing, a-al-le-lu-u-ia!

Where, O death, isnow thysti-ing?A-al-le-lu-u-ia!

Once He died oursoulstosave,a-al-le-lu-u-ia!

Where thy victory,Ograve? a-al-le-lu-u-ia!

Love's redeemingwork isdone, a-al-le-lu-u-ia!

Fought the fight, thebattlewon-on, a-al-le-lu-u-ia!

Death in vain forbidsHimrise,a-al-le-lu-u-ia!

Christ hath openedparadise, a-al-le-lu-u-ia!

For hymn background, this United Methodist Church history note is a good companion to the lead sheet.

The first line in the source looks like this:

{{
    lead_line(
        Christ_the_Lord_is='G',
        risen_to_day_2d='C',
        ay_2c_a_2d='G',
        al_2d='C',
        le_2d='C',
        lu_2d='G',
        u_2d='D7',
        ia_21='G',
    )
}}

That expands at build time into HTML like this:

<p class="lead-line">
    <span class="cell">
        <span class="chord-box" aria-label="G chord diagram" data-chord="G">
            <span data-pos="E3"></span>
            <span data-pos="A2"></span>
            <span data-pos="D0"></span>
            <span data-pos="G0"></span>
            <span data-pos="B0"></span>
            <span data-pos="e3"></span>
        </span>
        <span class="lyric">Christ the Lord is</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="C chord diagram" data-chord="C">
            <span data-pos="E0"></span>
            <span data-pos="A3"></span>
            <span data-pos="D2"></span>
            <span data-pos="G0"></span>
            <span data-pos="B1"></span>
            <span data-pos="e0"></span>
        </span>
        <span class="lyric">risen to day-</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="G chord diagram" data-chord="G">
            <span data-pos="E3"></span>
            <span data-pos="A2"></span>
            <span data-pos="D0"></span>
            <span data-pos="G0"></span>
            <span data-pos="B0"></span>
            <span data-pos="e3"></span>
        </span>
        <span class="lyric">ay, a-</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="C chord diagram" data-chord="C">
            <span data-pos="E0"></span>
            <span data-pos="A3"></span>
            <span data-pos="D2"></span>
            <span data-pos="G0"></span>
            <span data-pos="B1"></span>
            <span data-pos="e0"></span>
        </span>
        <span class="lyric">al-</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="C chord diagram" data-chord="C">
            <span data-pos="E0"></span>
            <span data-pos="A3"></span>
            <span data-pos="D2"></span>
            <span data-pos="G0"></span>
            <span data-pos="B1"></span>
            <span data-pos="e0"></span>
        </span>
        <span class="lyric">le-</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="G chord diagram" data-chord="G">
            <span data-pos="E3"></span>
            <span data-pos="A2"></span>
            <span data-pos="D0"></span>
            <span data-pos="G0"></span>
            <span data-pos="B0"></span>
            <span data-pos="e3"></span>
        </span>
        <span class="lyric">lu-</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="D7 chord diagram" data-chord="D7">
            <span data-pos="E0"></span>
            <span data-pos="A0"></span>
            <span data-pos="D0"></span>
            <span data-pos="G2"></span>
            <span data-pos="B1"></span>
            <span data-pos="e2"></span>
        </span>
        <span class="lyric">u-</span>
    </span>
    <span class="cell">
        <span class="chord-box" aria-label="G chord diagram" data-chord="G">
            <span data-pos="E3"></span>
            <span data-pos="A2"></span>
            <span data-pos="D0"></span>
            <span data-pos="G0"></span>
            <span data-pos="B0"></span>
            <span data-pos="e3"></span>
        </span>
        <span class="lyric">ia!</span>
    </span>
</p>

Happy Easter!

Fretboard Diagrams

Unicode includes an empty four-string fretboard, 𝄝, for ukulele or bass, and an empty six-string fretboard, 𝄜, for guitar. How are people supposed to compose the fretboard background and marks like ●, ◯, and ✕?

Going all the way back to ASCII art and box drawings, one approach is multiple lines: one column per string, one row per fret, top markers for muted and open strings, and filled dots where fingers land. The canonical left-to-right order for guitars is L, E, A, D, G, B, e, R.

Open C major: x32010

  E   A   D   G   B   e
  ✕           ◯       ◯
  ┌───┬───┬───┬───┬───┐
1 │   │   │   │   ●   │
  ├───┼───┼───┼───┼───┤
2 │   │   ●   │   │   │
  ├───┼───┼───┼───┼───┤
3 │   ●   │   │   │   │
  ├───┼───┼───┼───┼───┤
4 │   │   │   │   │   │
  └───┴───┴───┴───┴───┘

This needed a pre tag with a tuned line-height to reduce the vertical gaps.

<pre style="font-family: monospace; line-height: 1.275">
  E   A   D   G   B   e
  ✕           ◯       ◯
  ┌───┬───┬───┬───┬───┐
1 │   │   │   │   ●   │
  ├───┼───┼───┼───┼───┤
2 │   │   ●   │   │   │
  ├───┼───┼───┼───┼───┤
3 │   ●   │   │   │   │
  ├───┼───┼───┼───┼───┤
4 │   │   │   │   │   │
  └───┴───┴───┴───┴───┘
</pre>

A CSS grid diagram looks like this:

EADGBe 1 2 3 4
<style>
.fretboard-grid {
  display: inline-grid;
  grid-template-columns: repeat(7, 1.5rem);
  grid-template-rows: repeat(6, 1.5rem);
  margin: 1rem 0;
  font: 0.95rem/1 monospace;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-position:
    2.25rem 3rem,
    2.25rem 4.5rem,
    2.25rem 6rem,
    2.25rem 7.5rem,
    2.25rem calc(9rem - 1px),
    2.25rem 3rem,
    3.75rem 3rem,
    5.25rem 3rem,
    6.75rem 3rem,
    8.25rem 3rem,
    9.75rem 3rem;
  background-repeat: no-repeat;
  background-size:
    7.5rem 3px,
    7.5rem 1px,
    7.5rem 1px,
    7.5rem 1px,
    7.5rem 1px,
    1px 6rem,
    1px 6rem,
    1px 6rem,
    1px 6rem,
    1px 6rem,
    1px 6rem;
}

.fretboard-grid > span { display: grid; place-items: center; }
</style>

<div class="fretboard-grid" aria-label="Open C major chord diagram">
  <span></span><span>E</span><span>A</span><span>D</span><span>G</span><span>B</span><span>e</span>
  <span></span><span></span><span></span><span></span><span></span><span></span><span></span>
  <span>1</span><span></span><span></span><span></span><span></span><span></span><span></span>
  <span>2</span><span></span><span></span><span></span><span></span><span></span><span></span>
  <span>3</span><span></span><span></span><span></span><span></span><span></span><span></span>
  <span>4</span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>

A sparse data-pos version can skip the empty cells. The CSS ^= selector matches the starting string letter, and $= matches the ending row number, so A3 decodes to the A string and third fret. The string letters are supplemented with L and R for the margins, and the row numbers with H for the header row and 0 for the nut marker row, which holds and .

1 2 3 4 E A D G B e
<style>
.fretboard-pos {
  display: inline-grid;
  grid-template-columns: repeat(7, 1.5rem);
  grid-template-rows: repeat(6, 1.5rem);
  margin: 1rem 0;
  font: 0.95rem/1 monospace;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-position:
    2.25rem 3rem,
    2.25rem 4.5rem,
    2.25rem 6rem,
    2.25rem 7.5rem,
    2.25rem calc(9rem - 1px),
    2.25rem 3rem,
    3.75rem 3rem,
    5.25rem 3rem,
    6.75rem 3rem,
    8.25rem 3rem,
    9.75rem 3rem;
  background-repeat: no-repeat;
  background-size:
    7.5rem 3px,
    7.5rem 1px,
    7.5rem 1px,
    7.5rem 1px,
    7.5rem 1px,
    1px 6rem,
    1px 6rem,
    1px 6rem,
    1px 6rem,
    1px 6rem,
    1px 6rem;
}

.fretboard-pos > span { display: grid; place-items: center; }
.fretboard-pos > [data-pos^='L'] { grid-column: 1; }
.fretboard-pos > [data-pos^='E'] { grid-column: 2; }
.fretboard-pos > [data-pos^='A'] { grid-column: 3; }
.fretboard-pos > [data-pos^='D'] { grid-column: 4; }
.fretboard-pos > [data-pos^='G'] { grid-column: 5; }
.fretboard-pos > [data-pos^='B'] { grid-column: 6; }
.fretboard-pos > [data-pos^='e'] { grid-column: 7; }
.fretboard-pos > [data-pos$='H'] { grid-row: 1; }
.fretboard-pos > [data-pos$='0'] { grid-row: 2; }
.fretboard-pos > [data-pos$='1'] { grid-row: 3; }
.fretboard-pos > [data-pos$='2'] { grid-row: 4; }
.fretboard-pos > [data-pos$='3'] { grid-row: 5; }
.fretboard-pos > [data-pos$='4'] { grid-row: 6; }
</style>

<div class="fretboard-pos" aria-label="Open C major chord diagram">
  <span data-pos="L1">1</span>
  <span data-pos="L2">2</span>
  <span data-pos="L3">3</span>
  <span data-pos="L4">4</span>
  <span data-pos="EH">E</span>
  <span data-pos="E0"></span>
  <span data-pos="AH">A</span>
  <span data-pos="A3"></span>
  <span data-pos="DH">D</span>
  <span data-pos="D2"></span>
  <span data-pos="GH">G</span>
  <span data-pos="G0"></span>
  <span data-pos="BH">B</span>
  <span data-pos="B1"></span>
  <span data-pos="eH">e</span>
  <span data-pos="e0"></span>
</div>

A Unicode-fretboard variation can keep the sparse data-pos markup and place marks from the Noto Music source geometry. The six-string and four-string glyphs sit on the same integer grid: a 37 unit stroke, a 135 unit gap, and 172 unit center spacing in a 1000 units-per-em font.

1 2 3 4 E A D G B e
<style>
.fretboard-glyph {
  --glyph-scale: 7rem;
  --column-origin: calc(1.2rem + var(--glyph-scale) * 137 / 2000); /* left inset, source units */
  --gap: calc(var(--glyph-scale) * 172 / 1000); /* source units */
  --row-origin: calc(0.9rem + var(--glyph-scale) * 503 / 2000); /* top inset, source units */
  margin: 1rem 0;
  width: 8.7rem; /* fret-label column plus six-string glyph */
  height: 8.8rem; /* header, open row, four frets, bottom line */
  display: inline-block;
  position: relative;
}

.fretboard-glyph::before {
  content: '\1D11C'; /* MUSICAL SYMBOL SIX-STRING FRETBOARD */
  color: currentColor;
  font: var(--glyph-scale)/1 'Noto Music', sans-serif;
  left: 1.2rem; /* left inset */
  opacity: 0.32; /* fade glyph behind the marks */
  pointer-events: none;
  position: absolute;
  top: 0.9rem; /* top inset */
  z-index: 0;
}

.fretboard-glyph > span {
  font: 0.95rem/1 monospace; /* overlay text size and leading */
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 1;
}
.fretboard-glyph > [data-pos^='L'] { left: 0.3rem; transform: translateY(-50%); /* fret-label column */ }
.fretboard-glyph > [data-pos^='E'] { left: var(--column-origin); }
.fretboard-glyph > [data-pos^='A'] { left: calc(var(--column-origin) + var(--gap)); }
.fretboard-glyph > [data-pos^='D'] { left: calc(var(--column-origin) + var(--gap) * 2); }
.fretboard-glyph > [data-pos^='G'] { left: calc(var(--column-origin) + var(--gap) * 3); }
.fretboard-glyph > [data-pos^='B'] { left: calc(var(--column-origin) + var(--gap) * 4); }
.fretboard-glyph > [data-pos^='e'] { left: calc(var(--column-origin) + var(--gap) * 5); }
.fretboard-glyph > [data-pos$='H'] { top: 0.45rem; /* top inset - header adjustment */ }
.fretboard-glyph > [data-pos$='0'] { top: 1.2rem; /* top inset + nut adjustment */ }
.fretboard-glyph > [data-pos$='1'] { top: var(--row-origin); }
.fretboard-glyph > [data-pos$='2'] { top: calc(var(--row-origin) + var(--gap)); }
.fretboard-glyph > [data-pos$='3'] { top: calc(var(--row-origin) + var(--gap) * 2); }
.fretboard-glyph > [data-pos$='4'] { top: calc(var(--row-origin) + var(--gap) * 3); }
</style>

<div class="fretboard-glyph" aria-label="Open C major chord diagram">
  <span data-pos="L1">1</span>
  <span data-pos="L2">2</span>
  <span data-pos="L3">3</span>
  <span data-pos="L4">4</span>
  <span data-pos="EH">E</span>
  <span data-pos="E0"></span>
  <span data-pos="AH">A</span>
  <span data-pos="A3"></span>
  <span data-pos="DH">D</span>
  <span data-pos="D2"></span>
  <span data-pos="GH">G</span>
  <span data-pos="G0"></span>
  <span data-pos="BH">B</span>
  <span data-pos="B1"></span>
  <span data-pos="eH">e</span>
  <span data-pos="e0"></span>
</div>

These approaches also apply to musical staves.

Trihard

A barn quilt fiddle widget.

Each square carries four triangles in west (W), north (N), south (S), east (E) order. Each triangle cycles through background, foreground, and accent. Positions run A01 through Z99.

Wide Key = Equals Value Logging

Key = Equals Value

Wide key=value logging keeps information explicit, compact, and searchable. It uses named fields for most values; free-form text can still go on the right-hand side of msg=. It also avoids the complexity and visual noise of JavaScript Object Notation (JSON). Labeled Tab Separated Values (LTSV) keeps the same wide, one-line shape while using : colon and \t tab separators instead of = equals and space. Brandur wrote about logfmt in 2013: stable keys, small values, one request or task per line. Another description I've thought is appropriate for this format is "Wide INI (WINI)". In grep, journalctl, or Papertrail, searching for st=5 catches status codes 500..599 inclusive--with no parser in the loop. The Python structlog library exposes KV renderer and logfmt for this style directly, and so does python-logfmter.

Format Published Structured Key/Value Element Visually Noisy
Prose No ❌ None ❌ \n newline ❌ No ✅
INInitialization (INI) 1980s Yes ✅ = equals ✅ \n newline ❌ No ✅
Common Log Format (CLF) 1995 No ❌ None ❌ space ✅ 🤷
YAML Ain't Markup Language (YAML) 2001 Yes ✅ : colon ✅ \n newline plus indentation ❌ No ✅
JavaScript Object Notation (JSON) 2001 Yes ✅ : colon ✅ , comma ✅ Yes ❌
Labeled Tab-Separated Values (LTSV) 2012 Yes ✅ : colon ✅ \t tab ✅ No ✅
Tom's Obvious Markup Language (TOML) 2013 Yes ✅ = equals ✅ \n newline ❌ No ✅
logfmt 2013 Yes ✅ = equals ✅ space ✅ No ✅

Emit keys consistently to implement structured key=value logging. Most operational values are simple scalars. Serialize iterables and mappings compactly to remain simple: list=1,2,3 dict=1..2:buckle-my-shoe,3..4:knock-at-the-door. This follows the LTSV guidance of avoiding quotes and escapes to keep tooling requirements minimal. As an escape hatch, complex data can be stuffed into the value as Python literals or JSON. Reducing unnecessary punctuation also reduces LLM token usage.

Wide Events / Canonical Log Lines

Fewer lines are better, with one line per request (or task) as the ideal. Canonical log lines makes that operational case well: coherent context, easier streaming, and cleaner copy-paste to record incident details. Multiple lines per request (or task) make key consistency harder to maintain. LoggingSucks reaches the same practical conclusion from another angle: wide single-line records make debugging and operations efficient, although Boris Tane's unexplained choice of JSON over key=value adds punctuation and tool requirements where clarity should win.

Keys/Labels

An aspiration here is to combine Gunicorn and Django output into one consistent wide key=value log line. In the meantime, here is an example of configuring gunicorn with abbreviated keys:

access_log_format = '%(r)s st=%(s)s lb=%(h)s ip=%({x-forwarded-for}i)s rt=%(L)ss us=%(u)s rf=%(f)s'

Of course, abbreviating has downsides, so use whole words when they are a better choice or when you are unsure.

Abbr LTSV Recommend Description Apache nginx gunicorn
time Time the request was received %t $time_local %(t)s
lb or ip* host Nearest client IP %h $remote_addr %(h)s
ff or ip* forwardedfor Forwarded client IP %{X-Forwarded-For}i $http_x_forwarded_for %({x-forwarded-for}i)s
us user Remote user %u $remote_user %(u)s
em User email
req First line of request (method, uri, rows) %r $request %(r)s
method Request method %m $request_method %(m)s
uri Request URI %U%q $request_uri %(U)s%(q)s
protocol Requested Protocol (usually "HTTP/1.0" or "HTTP/1.1") %H $server_protocol %(H)s
st status Status code %>s $status %(s)s
size Size of response in bytes, excluding HTTP headers; suffix with b for explicit units %Bb (or %bb for compatibility with combined format) ${body_bytes_sent}b %(B)sb
reqsize Bytes received, including request and headers; suffix with b for explicit units %Ib (mod_log_io required) ${request_length}b %({content-length}i)sb
rf referer Referer header %{Referer}i $http_referer %(f)s
ua ua User-Agent header %{User-agent}i $http_user_agent %(a)s
hs vhost Host header %{Host}i $host %({host}i)s
reqtime_microsec The time taken to serve the request, in microseconds; suffix with us for explicit units %Dus %(D)sus
rt reqtime The time taken to serve the request, in seconds; suffix with s for explicit units %Ts ${request_time}s %(L)ss
cache X-Cache header %{X-Cache}o $upstream_http_x_cache %({x-cache}o)s
runtime Execution time for processing some request, e.g. X-Runtime header for application server or processing time of SQL for DB server. %{X-Runtime}o $upstream_http_x_runtime %({x-runtime}o)s
apptime Response time from the upstream server $upstream_response_time

* Replace lb or ff with ip based on presence or absence of load balancer.

Numbers 3

Numbers 3 (BSB)

Tent of Meeting

Numbers 2

The Order of the Camps

Numbers 2 (BSB)