<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: logging</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/logging.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-05-08T21:00:22+00:00</updated><author><name>Simon Willison</name></author><entry><title>Reservoir Sampling</title><link href="https://simonwillison.net/2025/May/8/reservoir-sampling/#atom-tag" rel="alternate"/><published>2025-05-08T21:00:22+00:00</published><updated>2025-05-08T21:00:22+00:00</updated><id>https://simonwillison.net/2025/May/8/reservoir-sampling/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://samwho.dev/reservoir-sampling/"&gt;Reservoir Sampling&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Yet another outstanding interactive essay by Sam Rose (&lt;a href="https://simonwillison.net/tags/sam-rose/"&gt;previously&lt;/a&gt;), this time explaining how reservoir sampling can be used to select a "fair" random sample when you don't know how many options there are and don't want to accumulate them before making a selection.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reservoir sampling is one of my favourite algorithms, and I've been wanting to write about it for years now. It allows you to solve a problem that at first seems impossible, in a way that is both elegant and efficient.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I appreciate that Sam starts the article with "No math notation, I promise." Lots of delightful widgets to interact with here, all of which help build an intuitive understanding of the underlying algorithm.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo. As a slider moves from left to right the probability of cards drawn from a deck is simulated. Text at the bottom reads Anything older than 15 cards ago is has a less than 0.01% chance of being held when I stop." src="https://static.simonwillison.net/static/2025/sam-rose-cards.gif" /&gt;&lt;/p&gt;
&lt;p&gt;Sam shows how this algorithm can be applied to the real-world problem of sampling log files when incoming logs threaten to overwhelm a log aggregator.&lt;/p&gt;
&lt;p&gt;The dog illustration is &lt;a href="https://samwho.dev/dogs/"&gt;commissioned art&lt;/a&gt; and the MIT-licensed code is &lt;a href="https://github.com/samwho/visualisations/tree/main/reservoir-sampling"&gt;available on GitHub&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=43928315"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/algorithms"&gt;algorithms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rate-limiting"&gt;rate-limiting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/explorables"&gt;explorables&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sam-rose"&gt;sam-rose&lt;/a&gt;&lt;/p&gt;



</summary><category term="algorithms"/><category term="logging"/><category term="rate-limiting"/><category term="explorables"/><category term="sam-rose"/></entry><entry><title>Queryable Logging with Blacklite</title><link href="https://simonwillison.net/2023/Aug/21/queryable-logging-with-blacklite/#atom-tag" rel="alternate"/><published>2023-08-21T18:13:35+00:00</published><updated>2023-08-21T18:13:35+00:00</updated><id>https://simonwillison.net/2023/Aug/21/queryable-logging-with-blacklite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tersesystems.com/blog/2020/11/26/queryable-logging-with-blacklite/"&gt;Queryable Logging with Blacklite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Will Sargent describes how he built Blacklite, a Java library for diagnostic logging that writes log events (as zstd compressed JSON objects) to a SQLite database and maintains 5,000 entries in a “live” database while entries beyond that range are cycled out to an archive.db file, which is cycled to archive.timestamp.db when it reaches 500,000 items.&lt;/p&gt;

&lt;p&gt;Lots of interesting notes here on using SQLite for high performance logging.&lt;/p&gt;

&lt;p&gt;“SQLite databases are also better log files in general. Queries are faster than parsing through flat files, with all the power of SQL. A vacuumed SQLite database is only barely larger than flat file logs. They are as easy to store and transport as flat file logs, but work much better when merging out of order or interleaved data between two logs.”

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://github.com/simonw/asgi-log-to-sqlite/issues/1#issuecomment-1518714493"&gt;asgi-log-to-sqlite/issues/1&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/java"&gt;java&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zstd"&gt;zstd&lt;/a&gt;&lt;/p&gt;



</summary><category term="java"/><category term="logging"/><category term="sqlite"/><category term="zstd"/></entry><entry><title>Replaying logs to exercise the new API</title><link href="https://simonwillison.net/2021/Mar/3/vaccinateca-2021-03-03/#atom-tag" rel="alternate"/><published>2021-03-03T17:00:00+00:00</published><updated>2021-03-03T17:00:00+00:00</updated><id>https://simonwillison.net/2021/Mar/3/vaccinateca-2021-03-03/#atom-tag</id><summary type="html">
    &lt;p class="context"&gt;&lt;em&gt;Originally posted to my internal blog at VaccinateCA&lt;/em&gt;&lt;/p&gt;&lt;p&gt;22 days ago &lt;a href="https://github.com/CAVaccineInventory/help.vaccinate/commit/0946d8196c8b5332c3a21dd1cd1fbd29c27037ef"&gt;n1mmy pushed a change&lt;/a&gt; to &lt;code&gt;help.vaccinate&lt;/code&gt; which logged full details of inoming Netlify function API traffic to an Airtable database.&lt;/p&gt;
&lt;p&gt;What an asset that is! The &lt;a href="https://airtable.com/tblvSiTbFMdCxv0Bq/viwJE9fQEfeHtPScq?blocks=hide" rel="nofollow"&gt;Airtable table over here&lt;/a&gt; currently contains over 9,000 logged API calls, including the full JSON POST body, when the call was receieved and which authenticated user made the call.&lt;/p&gt;
&lt;p&gt;This morning I exported that data as CSV from Airtable, and &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/blob/6d463148334f8b5c3f14c44561ea4b69efc08366/scripts/replay_api_logs_from_csv.py"&gt;wrote a Python script&lt;/a&gt; to replay those requests against my new imitation implementation of the API.&lt;/p&gt;
&lt;p&gt;Here's what that script looks like running against my localhost development server:&lt;/p&gt;
&lt;p&gt;&lt;img alt="import" src="https://user-images.githubusercontent.com/9599/109910446-fab60380-7c5c-11eb-9920-197d6c707853.gif" style="max-width:100%;"/&gt;&lt;/p&gt;
&lt;p&gt;You can track the work &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/issues/29"&gt;in this issue&lt;/a&gt; - the replay script helped me get to a place where every single report from the past 22 days can be safely ingested by the new API, with the exception of a tiny number of reports against locations which have since been deleted (which isn't supposed to happen - we try to soft-delete rather than full-delete things - but apparently a few deletes had slipped through).&lt;/p&gt;
&lt;h4&gt;
API logging&lt;/h4&gt;
&lt;p&gt;Since the Airtable API logs have so clearly proved their value, Jesse proposed &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/issues/24"&gt;using the same trick&lt;/a&gt; for the Django app. I implemented that today: the full incoming request body and outgoing response are now recorded in an &lt;code&gt;ApiLog&lt;/code&gt; model in Django. You can see those in the Django admin here: &lt;a href="https://vaccinateca-preview.herokuapp.com/admin/api/apilog/" rel="nofollow"&gt;https://vaccinateca-preview.herokuapp.com/admin/api/apilog/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Select_api_log_to_change___Django_site_admin" src="https://user-images.githubusercontent.com/9599/109911028-2e455d80-7c5e-11eb-9644-8deefd80b580.png" style="max-width:100%;"/&gt; &lt;img alt="Change_api_log___Django_site_admin" src="https://user-images.githubusercontent.com/9599/109910821-d9094c00-7c5d-11eb-9b00-e3867dfc5796.png" style="max-width:100%;"/&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/blob/6d463148334f8b5c3f14c44561ea4b69efc08366/vaccinate/api/models.py"&gt;the ORM model&lt;/a&gt; and the &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/blob/6d463148334f8b5c3f14c44561ea4b69efc08366/vaccinate/api/utils.py"&gt;view decorator&lt;/a&gt; that logs requests.&lt;/p&gt;
&lt;h4&gt;
Unit tests for the new API&lt;/h4&gt;
&lt;p&gt;I added tests for the &lt;code&gt;submitReport&lt;/code&gt; API. The tests are driven by example JSON fixtures - so far I've created two of those, but I hope that having them in this format will make it really easy to add more as we find edge-cases in the API and expand it with new features.&lt;/p&gt;
&lt;p&gt;Those API test fixtures live in &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/tree/6d463148334f8b5c3f14c44561ea4b69efc08366/vaccinate/api/test-data/submitReport"&gt;vaccinate/api/test-data/submitReport&lt;/a&gt;. Here's &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/blob/6d463148334f8b5c3f14c44561ea4b69efc08366/vaccinate/api/test_submit_report.py#L38-L73"&gt;the test code&lt;/a&gt; that executes them.&lt;/p&gt;
&lt;h4&gt;
Documentation for the new API&lt;/h4&gt;
&lt;p&gt;I wrote API documentation! You can &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/blob/6d463148334f8b5c3f14c44561ea4b69efc08366/docs/api.md"&gt;find that here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now that the API is documented I intend to update the documentation in lock-step with changes made to the API itself - using the pattern where every commit includes the change, the tests for the change AND the documentation for the change in a single unit.&lt;/p&gt;
&lt;h4&gt;
Dual-writing to Django&lt;/h4&gt;
&lt;p&gt;The combination of the replay script and the unit tests has left me feeling pretty confident that the replacement API is ready to start accepting traffic.&lt;/p&gt;
&lt;p&gt;The plan is to run the new system in parallel with Airtable for a few days to thoroughly test it and make sure it covers everything we need. Our Netlify functions offer a great place to do this, so this afternoon I submitted &lt;a href="https://github.com/CAVaccineInventory/help.vaccinate/pull/77"&gt;a pull request&lt;/a&gt; to &lt;code&gt;help.vaccinate&lt;/code&gt; to silently dual-write incoming API requests to the Django API, catching and logging any exceptions without intefering with the rest of the application flow.&lt;/p&gt;
&lt;p&gt;Testing this locally helped me identify some bugs in the way the Django app verified JWT tokens that originated with the &lt;code&gt;help.vaccinate&lt;/code&gt; application.&lt;/p&gt;
&lt;h4&gt;
Everything else&lt;/h4&gt;
&lt;p&gt;Here are &lt;a href="https://github.com/CAVaccineInventory/django.vaccinate/commits/6d463148334f8b5c3f14c44561ea4b69efc08366"&gt;my other commits from today&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccinate-ca"&gt;vaccinate-ca&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccinate-ca-blog"&gt;vaccinate-ca-blog&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="apis"/><category term="logging"/><category term="vaccinate-ca"/><category term="vaccinate-ca-blog"/></entry><entry><title>Logging to SQLite using ASGI middleware</title><link href="https://simonwillison.net/2019/Dec/16/logging-sqlite-asgi-middleware/#atom-tag" rel="alternate"/><published>2019-12-16T22:30:46+00:00</published><updated>2019-12-16T22:30:46+00:00</updated><id>https://simonwillison.net/2019/Dec/16/logging-sqlite-asgi-middleware/#atom-tag</id><summary type="html">
    &lt;p&gt;I had some fun playing around with &lt;a href="https://asgi.readthedocs.io/en/latest/specs/main.html#middleware"&gt;ASGI middleware&lt;/a&gt; and logging during our flight back to England for the holidays.&lt;/p&gt;
&lt;h3 id="asgi-log-to-sqlite"&gt;asgi-log-to-sqlite&lt;/h3&gt;
&lt;p&gt;I decided to experiment with SQLite as a logging mechanism. I wouldn’t use this on a high traffic site, but most of my Datasette related projects are small enough that logging HTTP traffic directly to a SQLite database feels like it should work reasonable well.&lt;/p&gt;
&lt;p&gt;Once your logs are in a SQLite database, you can use &lt;a href="https://datasette.readthedocs.io/"&gt;Datasette&lt;/a&gt; to analyze them. I think this could be a lot of fun.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/asgi-log-to-sqlite"&gt;asgi-log-to-sqlite&lt;/a&gt; is my first exploration of this idea. It’s a piece of ASGI middleware which wraps an ASGI application and then logs relevant information from the request and response to an attached SQLite database.&lt;/p&gt;
&lt;p&gt;You use it like this:&lt;/p&gt;
&lt;pre class=" language-python"&gt;&lt;code class="prism  language-python"&gt;&lt;span class="token keyword"&gt;from&lt;/span&gt; asgi_log_to_sqlite &lt;span class="token keyword"&gt;import&lt;/span&gt; AsgiLogToSqlite
&lt;span class="token keyword"&gt;from&lt;/span&gt; my_asgi_app &lt;span class="token keyword"&gt;import&lt;/span&gt; app

app &lt;span class="token operator"&gt;=&lt;/span&gt; AsgiLogToSqlite&lt;span class="token punctuation"&gt;(&lt;/span&gt;app&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"/tmp/log.db"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s a demo Datasette instance showing logs from my testing: &lt;a href="https://asgi-log-demo-j7hipcg4aq-uc.a.run.app"&gt;asgi-log-demo-j7hipcg4aq-uc.a.run.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As always with Datasette, the data is at its most interesting once you &lt;a href="https://asgi-log-demo-j7hipcg4aq-uc.a.run.app/asgi-log-demo/requests?_sort_desc=rowid&amp;amp;_facet=path&amp;amp;_facet=user_agent&amp;amp;_facet=content_type#facet-content_type"&gt;apply some facets&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="intercepting-requests-to-and-from-the-wrapped-asgi-app"&gt;Intercepting requests to and from the wrapped ASGI app&lt;/h3&gt;
&lt;p&gt;There are a couple of interesting parts of the implementation. The first is how the information is gathered from the request and response.&lt;/p&gt;
&lt;p&gt;This is a classic pattern for ASGI middleware. &lt;a href="https://asgi.readthedocs.io/en/latest/specs/main.html#applications"&gt;The ASGI protocol&lt;/a&gt; has three key components; a &lt;code&gt;scope&lt;/code&gt; dictionary describing the incoming request, and two async functions called &lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; which are used to retrieve and send data to the connected client (usually a browser).&lt;/p&gt;
&lt;p&gt;Most middleware works by wrapping those functions with custom replacements. That’s what I’m doing here:&lt;/p&gt;
&lt;pre class=" language-python"&gt;&lt;code class="prism  language-python"&gt;&lt;span class="token keyword"&gt;class&lt;/span&gt; &lt;span class="token class-name"&gt;AsgiLogToSqlite&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;__init__&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;self&lt;span class="token punctuation"&gt;,&lt;/span&gt; app&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token builtin"&gt;file&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
        self&lt;span class="token punctuation"&gt;.&lt;/span&gt;app &lt;span class="token operator"&gt;=&lt;/span&gt; app
        self&lt;span class="token punctuation"&gt;.&lt;/span&gt;db &lt;span class="token operator"&gt;=&lt;/span&gt; sqlite_utils&lt;span class="token punctuation"&gt;.&lt;/span&gt;Database&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token builtin"&gt;file&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token comment"&gt;# ...&lt;/span&gt;
    &lt;span class="token keyword"&gt;async&lt;/span&gt; &lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;__call__&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;self&lt;span class="token punctuation"&gt;,&lt;/span&gt; scope&lt;span class="token punctuation"&gt;,&lt;/span&gt; receive&lt;span class="token punctuation"&gt;,&lt;/span&gt; send&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
        response_headers &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
        body_size &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;
        http_status &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token boolean"&gt;None&lt;/span&gt;

        &lt;span class="token keyword"&gt;async&lt;/span&gt; &lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;wrapped_send&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;message&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
            &lt;span class="token keyword"&gt;nonlocal&lt;/span&gt; body_size&lt;span class="token punctuation"&gt;,&lt;/span&gt; response_headers&lt;span class="token punctuation"&gt;,&lt;/span&gt; http_status
            &lt;span class="token keyword"&gt;if&lt;/span&gt; message&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"type"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;==&lt;/span&gt; &lt;span class="token string"&gt;"http.response.start"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
                response_headers &lt;span class="token operator"&gt;=&lt;/span&gt; message&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"headers"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
                http_status &lt;span class="token operator"&gt;=&lt;/span&gt; message&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"status"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;

            &lt;span class="token keyword"&gt;if&lt;/span&gt; message&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"type"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;==&lt;/span&gt; &lt;span class="token string"&gt;"http.response.body"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
                body_size &lt;span class="token operator"&gt;+=&lt;/span&gt; &lt;span class="token builtin"&gt;len&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;message&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"body"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;

            &lt;span class="token keyword"&gt;await&lt;/span&gt; send&lt;span class="token punctuation"&gt;(&lt;/span&gt;message&lt;span class="token punctuation"&gt;)&lt;/span&gt;

        start &lt;span class="token operator"&gt;=&lt;/span&gt; time&lt;span class="token punctuation"&gt;.&lt;/span&gt;time&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token keyword"&gt;await&lt;/span&gt; self&lt;span class="token punctuation"&gt;.&lt;/span&gt;app&lt;span class="token punctuation"&gt;(&lt;/span&gt;scope&lt;span class="token punctuation"&gt;,&lt;/span&gt; receive&lt;span class="token punctuation"&gt;,&lt;/span&gt; wrapped_send&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        end &lt;span class="token operator"&gt;=&lt;/span&gt; time&lt;span class="token punctuation"&gt;.&lt;/span&gt;time&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My &lt;code&gt;wrapped_send()&lt;/code&gt; function replaces the original &lt;code&gt;send()&lt;/code&gt; function with one that pulls out some of the data I want to log from the messages that are being sent to the client.&lt;/p&gt;
&lt;p&gt;I record a start time, then &lt;code&gt;await&lt;/code&gt; the original ASGI application, then record an end time when it finishes.&lt;/p&gt;
&lt;h3 id="logging-to-sqlite-using-sqlite-utils"&gt;Logging to SQLite using sqlite-utils&lt;/h3&gt;
&lt;p&gt;I’m using my &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/python-api.html"&gt;sqlite-utils library&lt;/a&gt; to implement the logging. My first version looked like this:&lt;/p&gt;
&lt;pre class=" language-python"&gt;&lt;code class="prism  language-python"&gt;db&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"requests"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;insert&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token string"&gt;"path"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; scope&lt;span class="token punctuation"&gt;.&lt;/span&gt;get&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"path"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token string"&gt;"response_headers"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;str&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;response_headers&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token string"&gt;"body_size"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; body_size&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token string"&gt;"http_status"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; http_status&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token string"&gt;"scope"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;str&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;scope&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; alter&lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token boolean"&gt;True&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; automatically creates a table with the correct schema the first time you try to insert a record into it. This makes it ideal for rapid prototyping. In this case I captured stringified versions of various data structures so I could look at them in my browser with Datasette.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;alter=True&lt;/code&gt; argument here means that if I attempt to insert a new shape of record into an existing tables any missing columns will be added automatically as well. Again, handy for prototyping.&lt;/p&gt;
&lt;p&gt;Based on the above, I evolved the code into recording the values I wanted to see in my logs - the full URL path, the User-Agent, the HTTP referrer, the IP and so on.&lt;/p&gt;
&lt;p&gt;This resulted in a LOT of duplicative data. Values like the path, user-agent and HTTP referrer are the same across many different requests.&lt;/p&gt;
&lt;p&gt;Regular plain text logs can solve this with gzip compression, but you can’t gzip a SQLite database and still expect it to work.&lt;/p&gt;
&lt;p&gt;Since we are logging to a relational database, we can solve for duplicate values using normalization. We can extract out those lengthy strings into separate lookup tables - that way we can store mostly integer foreign key references in the requests table itself.&lt;/p&gt;
&lt;p&gt;After a few iterations, my database code ended up looking like this:&lt;/p&gt;
&lt;pre class=" language-python"&gt;&lt;code class="prism  language-python"&gt;&lt;span class="token keyword"&gt;with&lt;/span&gt; db&lt;span class="token punctuation"&gt;.&lt;/span&gt;conn&lt;span class="token punctuation"&gt;:&lt;/span&gt;  &lt;span class="token comment"&gt;# Use a transaction&lt;/span&gt;
    db&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"requests"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;insert&lt;span class="token punctuation"&gt;(&lt;/span&gt;
        &lt;span class="token punctuation"&gt;{&lt;/span&gt;
            &lt;span class="token string"&gt;"start"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; start&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"method"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; scope&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"method"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"path"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"paths"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; path&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"query_string"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"query_strings"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; query_string&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"user_agent"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"user_agents"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; user_agent&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"referer"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"referers"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; referer&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"accept_language"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"accept_languages"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; accept_language&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"http_status"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; http_status&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"content_type"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"content_types"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; content_type&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"client_ip"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; scope&lt;span class="token punctuation"&gt;.&lt;/span&gt;get&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"client"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token boolean"&gt;None&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token boolean"&gt;None&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"duration"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; end &lt;span class="token operator"&gt;-&lt;/span&gt; start&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"body_size"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; body_size&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        alter&lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token boolean"&gt;True&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        foreign_keys&lt;span class="token operator"&gt;=&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;lookup_columns&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;)&lt;/span&gt;


&lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;lookup&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;db&lt;span class="token punctuation"&gt;,&lt;/span&gt; table&lt;span class="token punctuation"&gt;,&lt;/span&gt; value&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token keyword"&gt;return&lt;/span&gt; db&lt;span class="token punctuation"&gt;[&lt;/span&gt;table&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;lookup&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token string"&gt;"name"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; value
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token keyword"&gt;if&lt;/span&gt; value &lt;span class="token keyword"&gt;else&lt;/span&gt; &lt;span class="token boolean"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/python-api.html#working-with-lookup-tables"&gt;table.lookup() method&lt;/a&gt; in &lt;code&gt;sqlite-utils&lt;/code&gt; is designed for exactly this use-case. If you pass it a value (or multiple values) it will ensure the underlying table has those columns with a unique index on them, then get-or-insert your data and return you the primary key.&lt;/p&gt;
&lt;p&gt;Automatically creating tables is fine for an initial prototype, but it starts getting a little messy once you have foreign keys relationships that you need to be able to rely on. I moved to explicit table creation in &lt;a href="https://github.com/simonw/asgi-log-to-sqlite/blob/5e58e577fea4bd99a7ae5e61b8d389684d55389c/asgi_log_to_sqlite.py#L21-L43"&gt;an ensure_tables() method&lt;/a&gt; that’s called once when the middleware class is used to wrap the underlying ASGI app:&lt;/p&gt;
&lt;pre class=" language-python"&gt;&lt;code class="prism  language-python"&gt;    lookup_columns &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;
        &lt;span class="token string"&gt;"path"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token string"&gt;"user_agent"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token string"&gt;"referer"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token string"&gt;"accept_language"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token string"&gt;"content_type"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token string"&gt;"query_string"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;)&lt;/span&gt;

    &lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;ensure_tables&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;self&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
        &lt;span class="token keyword"&gt;for&lt;/span&gt; column &lt;span class="token keyword"&gt;in&lt;/span&gt; self&lt;span class="token punctuation"&gt;.&lt;/span&gt;lookup_columns&lt;span class="token punctuation"&gt;:&lt;/span&gt;
            table &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string"&gt;"{}s"&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token builtin"&gt;format&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;column&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token operator"&gt;not&lt;/span&gt; self&lt;span class="token punctuation"&gt;.&lt;/span&gt;db&lt;span class="token punctuation"&gt;[&lt;/span&gt;table&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;exists&lt;span class="token punctuation"&gt;:&lt;/span&gt;
                self&lt;span class="token punctuation"&gt;.&lt;/span&gt;db&lt;span class="token punctuation"&gt;[&lt;/span&gt;table&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;create&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
                    &lt;span class="token string"&gt;"id"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                    &lt;span class="token string"&gt;"name"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;str&lt;/span&gt;
                &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; pk&lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token string"&gt;"id"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token operator"&gt;not&lt;/span&gt; self&lt;span class="token punctuation"&gt;.&lt;/span&gt;db&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"requests"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;exists&lt;span class="token punctuation"&gt;:&lt;/span&gt;
            self&lt;span class="token punctuation"&gt;.&lt;/span&gt;db&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"requests"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;create&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
                &lt;span class="token string"&gt;"start"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;float&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"method"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;str&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"path"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"query_string"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"user_agent"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"referer"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"accept_language"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"http_status"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"content_type"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"client_ip"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;str&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"duration"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;float&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                &lt;span class="token string"&gt;"body_size"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;int&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; foreign_keys&lt;span class="token operator"&gt;=&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;lookup_columns&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’m increasingly using this pattern in my &lt;code&gt;sqlite-utils&lt;/code&gt; projects. It’s not a full-grown migrations system but it’s a pretty low-effort way of creating tables correctly provided they don’t yet exist.&lt;/p&gt;
&lt;p&gt;Here’s &lt;a href="https://github.com/simonw/asgi-log-to-sqlite/blob/5e58e577fea4bd99a7ae5e61b8d389684d55389c/asgi_log_to_sqlite.py"&gt;the full implementation of the middleware&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="configuring-the-middleware-for-use-with-datasette"&gt;Configuring the middleware for use with Datasette&lt;/h3&gt;
&lt;p&gt;Publishing standalone ASGI middleware for this kind of thing is neat because it can be used with any ASGI application, not just with Datasette.&lt;/p&gt;
&lt;p&gt;To make it as usable as possible with Datasette I want it made available as a plugin.&lt;/p&gt;
&lt;p&gt;I’ve tried two different patterns for this in the past.&lt;/p&gt;
&lt;p&gt;My first ASGI middleware was &lt;a href="https://github.com/simonw/asgi-cors"&gt;asgi-cors&lt;/a&gt;. I published that as two separate packages to PyPI: &lt;code&gt;asgi-cors&lt;/code&gt; is the middleware itself, and &lt;a href="https://github.com/simonw/asgi-cors"&gt;datasette-cors&lt;/a&gt; is a very thin plugin wrapper around it that hooks into Datasette’s &lt;a href="https://datasette.readthedocs.io/en/0.32/plugins.html#plugin-configuration"&gt;plugin configuration mechanism&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For &lt;a href="https://github.com/simonw/datasette-auth-github"&gt;datasette-auth-github&lt;/a&gt; I decided not to publish two packages. Instead I published a single plugin package and then described how to use it as standalone ASGI middleware in its documentation.&lt;/p&gt;
&lt;p&gt;This lazier approach is confusing: it’s not at all clear that a package called &lt;code&gt;datasette-auth-github&lt;/code&gt; can be used independently of Datasette. But I did get to avoid having to publish two packages.&lt;/p&gt;
&lt;h3 id="datasette-configure-asgi"&gt;datasette-configure-asgi&lt;/h3&gt;
&lt;p&gt;Since I want to do a lot more experiments with ASGI plugins in the future, I decided to try solving the ASGI configuration issue once and for all. I built a new experimental plugin, &lt;a href="https://github.com/simonw/datasette-configure-asgi"&gt;datasette-configure-asgi&lt;/a&gt; which can be used to configure ANY ASGI middleware that conforms to an expected protocol.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like at the configuration level, using &lt;a href="https://datasette.readthedocs.io/en/0.32/metadata.html"&gt;a metadata.json&lt;/a&gt; settings file (which &lt;a href="https://github.com/simonw/datasette/issues/493"&gt;I should really rename&lt;/a&gt; since it’s more about configuration than metadata these days):&lt;/p&gt;
&lt;pre class=" language-json"&gt;&lt;code class="prism  language-json"&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token string"&gt;"plugins"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token string"&gt;"datasette-configure-asgi"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;
      &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token string"&gt;"class"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"asgi_log_to_sqlite.AsgiLogToSqlite"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token string"&gt;"args"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
          &lt;span class="token string"&gt;"file"&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"/tmp/log.db"&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;
      &lt;span class="token punctuation"&gt;}&lt;/span&gt;
    &lt;span class="token punctuation"&gt;]&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation of this plugin is very simple: here’s the entire thing:&lt;/p&gt;
&lt;pre class=" language-python"&gt;&lt;code class="prism  language-python"&gt;&lt;span class="token keyword"&gt;from&lt;/span&gt; datasette &lt;span class="token keyword"&gt;import&lt;/span&gt; hookimpl
&lt;span class="token keyword"&gt;import&lt;/span&gt; importlib


@hookimpl
&lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;asgi_wrapper&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;datasette&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token keyword"&gt;def&lt;/span&gt; &lt;span class="token function"&gt;wrap_with_classes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;app&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
        configs &lt;span class="token operator"&gt;=&lt;/span&gt; datasette&lt;span class="token punctuation"&gt;.&lt;/span&gt;plugin_config&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"datasette-configure-asgi"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;or&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
        &lt;span class="token keyword"&gt;for&lt;/span&gt; config &lt;span class="token keyword"&gt;in&lt;/span&gt; configs&lt;span class="token punctuation"&gt;:&lt;/span&gt;
            module_path&lt;span class="token punctuation"&gt;,&lt;/span&gt; class_name &lt;span class="token operator"&gt;=&lt;/span&gt; config&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"class"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;rsplit&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"."&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            mod &lt;span class="token operator"&gt;=&lt;/span&gt; importlib&lt;span class="token punctuation"&gt;.&lt;/span&gt;import_module&lt;span class="token punctuation"&gt;(&lt;/span&gt;module_path&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            klass &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token builtin"&gt;getattr&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;mod&lt;span class="token punctuation"&gt;,&lt;/span&gt; class_name&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            args &lt;span class="token operator"&gt;=&lt;/span&gt; config&lt;span class="token punctuation"&gt;.&lt;/span&gt;get&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"args"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;or&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;span class="token punctuation"&gt;}&lt;/span&gt;
            app &lt;span class="token operator"&gt;=&lt;/span&gt; klass&lt;span class="token punctuation"&gt;(&lt;/span&gt;app&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token operator"&gt;**&lt;/span&gt;args&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token keyword"&gt;return&lt;/span&gt; app

    &lt;span class="token keyword"&gt;return&lt;/span&gt; wrap_with_classes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It hooks into the &lt;a href="https://datasette.readthedocs.io/en/0.32/plugins.html#asgi-wrapper-datasette"&gt;asgi_wrapper plugin hook&lt;/a&gt;, reads its configuration from the &lt;code&gt;datasette&lt;/code&gt; object (using &lt;a href="https://datasette.readthedocs.io/en/0.32/plugins.html#writing-plugins-that-accept-configuration"&gt;plugin_config()&lt;/a&gt;), then loops through the list of configured plugins and dynamically loads each implementation using &lt;a href="https://docs.python.org/3/library/importlib.html#importlib.import_module"&gt;importlib&lt;/a&gt;. Then it wraps the ASGI app with each of them in turn.&lt;/p&gt;
&lt;h3 id="open-questions"&gt;Open questions&lt;/h3&gt;
&lt;p&gt;This is where I’ve got to with my experiments so far. Should you use this stuff in production? Almost certainly not! I wrote it on a plane just now. It definitely needs a bit more thought.&lt;/p&gt;
&lt;p&gt;A couple of obvious open questions:&lt;/p&gt;
&lt;p&gt;Python async functions &lt;strong&gt;shouldn’t make blocking calls&lt;/strong&gt;, since doing so will block the entire event loop for everyone else.&lt;/p&gt;
&lt;p&gt;Interacting with SQLite is a blocking call. Datasette works around this by &lt;a href="https://github.com/simonw/datasette/blob/d6b6c9171f3fd945c4e5e4144923ac831c43c208/datasette/database.py#L56-L67"&gt;running SQL queries in a thread pool&lt;/a&gt;; my logging plugin doesn’t bother with that.&lt;/p&gt;
&lt;p&gt;Maybe it should? My hunch is that inserting into SQLite in this way is so fast it won’t actually cause any noticeable overhead. It would be nice to test that assumption thoroughly though.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Log rotation&lt;/strong&gt;. This is an important detail for any well designed logging system, and I’ve punted on it entirely. Figuring out an elegant way to handle this with underlying SQLite databases files would be an interesting design challenge - &lt;a href="https://github.com/simonw/asgi-log-to-sqlite/issues/1"&gt;relevant issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Would my SQLite logging middleware work with &lt;strong&gt;Django 3.0&lt;/strong&gt;? I don’t see why not - the documentation covers &lt;a href="https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/#applying-asgi-middleware"&gt;how to wrap entire Django applications with ASGI middleware&lt;/a&gt;. I should try that out!&lt;/p&gt;
&lt;h3 id="this-weeks-niche-museums"&gt;This week’s Niche Museums&lt;/h3&gt;
&lt;p&gt;These are technically my weeknotes, but logging experiments aside it’s been a quiet week for me.&lt;/p&gt;
&lt;p&gt;I finally added paragraph breaks to &lt;a href="https://www.niche-museums.com/"&gt;Niche Museums&lt;/a&gt; (using &lt;a href="https://github.com/simonw/datasette-render-markdown"&gt;datasette-render-markdown&lt;/a&gt;, implementation &lt;a href="https://github.com/simonw/museums/commit/a9e105196e4987710bc982837bfda24a7aefebeb"&gt;here&lt;/a&gt;) As a result my descriptions have been getting a whole lot longer. Added this week:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/61"&gt;The Tonga Room&lt;/a&gt; in San Francisco&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/62"&gt;London Silver Vaults&lt;/a&gt; in London&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/63"&gt;Rosie the Riveter National Historical Park&lt;/a&gt; in Richmond, CA&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/64"&gt;LA Bureau of Street Lighting Museum&lt;/a&gt; in Los Angeles&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/65"&gt;Aye-Aye Island&lt;/a&gt; in Madagascar&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/66"&gt;Monarch Bear Grove&lt;/a&gt; in San Francisco&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/67"&gt;Alverstone Mead Red Squirrel Hide&lt;/a&gt; on the Isle of Wight&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prototyping"&gt;prototyping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="logging"/><category term="projects"/><category term="prototyping"/><category term="sqlite"/><category term="datasette"/><category term="asgi"/><category term="weeknotes"/><category term="sqlite-utils"/></entry><entry><title>Logs vs. metrics: a false dichotomy</title><link href="https://simonwillison.net/2019/Aug/3/logs-vs-metrics/#atom-tag" rel="alternate"/><published>2019-08-03T16:46:55+00:00</published><updated>2019-08-03T16:46:55+00:00</updated><id>https://simonwillison.net/2019/Aug/3/logs-vs-metrics/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://whiteink.com/2019/logs-vs-metrics-a-false-dichotomy/"&gt;Logs vs. metrics: a false dichotomy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Nick Stenning discusses the differences between logs and metrics: most notably that metrics can be derived from logs but logs cannot be reconstituted starting with time-series metrics.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mipsytipsy/status/1157503142134607872"&gt;Charity Majors&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observability"&gt;observability&lt;/a&gt;&lt;/p&gt;



</summary><category term="logging"/><category term="observability"/></entry><entry><title>Targeted diagnostic logging in production</title><link href="https://simonwillison.net/2019/Jul/24/targeted-diagnostic-logging-production/#atom-tag" rel="alternate"/><published>2019-07-24T05:44:39+00:00</published><updated>2019-07-24T05:44:39+00:00</updated><id>https://simonwillison.net/2019/Jul/24/targeted-diagnostic-logging-production/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tersesystems.com/blog/2019/07/22/targeted-diagnostic-logging-in-production/"&gt;Targeted diagnostic logging in production&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Will Sargent defines diagnostic logging as “debug logging statements with an audience”, and proposes controlling this style if logging via a feature flat system to allow detailed logging to be turned on in production against a selected subset if users in order to help debug difficult problems. Lots of great background material in the topic of observability here too.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mipsytipsy/status/1153889935536975872"&gt;Charity Majors&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observability"&gt;observability&lt;/a&gt;&lt;/p&gt;



</summary><category term="logging"/><category term="observability"/></entry><entry><title>Running gunicorn behind nginx on Heroku for buffering and logging</title><link href="https://simonwillison.net/2017/Oct/2/nginx-heroku/#atom-tag" rel="alternate"/><published>2017-10-02T01:57:20+00:00</published><updated>2017-10-02T01:57:20+00:00</updated><id>https://simonwillison.net/2017/Oct/2/nginx-heroku/#atom-tag</id><summary type="html">
    &lt;p&gt;Heroku's default setup for Django uses the &lt;a href="http://gunicorn.org/"&gt;gunicorn&lt;/a&gt; application server. Each
Heroku dyno can only run a limited number of gunicorn workers, which means a
limited number of requests can be served in parallel (around 4 per dyno is a
good rule of thumb).&lt;/p&gt;

&lt;p&gt;Where things get nasty is when you have devices on slow connections - like
mobile phones. Heroku's router buffers headers but it does not buffer response
bodies, so a slow device could hold up a gunicorn worker for several seconds.
Too many slow devices at once and the site will become unavailable to other
users.&lt;/p&gt;

&lt;p&gt;This issue is explained and discussed here: &lt;a href="http://blog.etianen.com/blog/2014/01/19/gunicorn-heroku-django/"&gt;Don't use Gunicorn to host your Django sites on Heroku &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That article recommends using waitress as an alternative to gunicorn, but in
the comments at the bottom of the article people suggest using a Heroku
&lt;a href="https://github.com/beanieboi/nginx-buildpack"&gt;nginx-buildpack&lt;/a&gt; as an alternative.&lt;/p&gt;

&lt;p&gt;Here is a slightly out-of-date tutorial on getting this all set up: &lt;a href="https://koed00.github.io/Heroku_setups/"&gt;https://koed00.github.io/Heroku_setups/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used the following commands to set up the buildpacks:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;heroku stack:set cedar-14
heroku buildpacks:clear
heroku buildpacks:add https://github.com/beanieboi/nginx-buildpack.git
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-python.git
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Unfortunately the nginx buildpack is not yet compatible with the new &lt;samp&gt;heroku-16&lt;/samp&gt;
stack, so until the nginx buildpack has been updated it's necessary to run the
application on the older &lt;samp&gt;cedar-14&lt;/samp&gt; stack. See this discussion for details: &lt;a href="https://github.com/ryandotsmith/nginx-buildpack/issues/68"&gt;ryandotsmith/nginx-buildpack#68&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding nginx in this way also gives us the opportunity to fix another
limitation of Heroku: its default logging configuration. By default, log lines produced by Heroku (visible using &lt;samp&gt;heroku logs --tail&lt;/samp&gt; or with a logging addon such as &lt;a href="https://elements.heroku.com/addons/papertrail"&gt;Papertrail&lt;/a&gt;) look like
this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    Oct 01 18:01:06 simonwillisonblog heroku/router: at=info
        method=GET path="/2017/Oct/1/ship/" host=simonwillison.net
        request_id=bb22f67e-6924-4e81-b6ad-74d1f465cda7
        fwd="2001:8003:74c5:8b00:79e4:80ed:fa85:7b37,108.162.249.198"
        dyno=web.1 connect=0ms service=338ms status=200 bytes=4523 protocol=http
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notably missing here is both the user-agent string and the referrer header
sent by the browser! If you're a fan of tailing log files these omissions are pretty
disappointing.&lt;/p&gt;

&lt;p&gt;The nginx buildback I'm using loads a default configuration file at
&lt;samp&gt;config/nginx.conf.erb&lt;/samp&gt;. By including &lt;a href="https://github.com/simonw/simonwillisonblog/blob/ad874a2bf9ebfeffcb0a1a7f8594ad9735fcfc01/config/nginx.conf.erb"&gt;my own copy of this file&lt;/a&gt; I can override
the original and define my own custom log format.&lt;/p&gt;

&lt;p&gt;Having applied this change, the new log lines look like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    2017-10-02T01:44:38.762845+00:00 app[web.1]:
        measure#nginx.service=0.133 request="GET / HTTP/1.1" status_code=200
        request_id=8b6402de-d072-42c4-9854-0f71697b30e5 remote_addr="10.16.227.159"
        forwarded_for="199.188.193.220" forwarded_proto="http" via="1.1 vegur"
        body_bytes_sent=12666 referer="-" user_agent="Mozilla/5.0 (Macintosh;
        Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko)
        Chrome/61.0.3163.100 Safari/537.36"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;This blog entry started life as &lt;a href="https://github.com/simonw/simonwillisonblog/commit/23615a4822ab463c611a3e6a1f4d6cb4dcfc5e7b"&gt;a commit message&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nginx"&gt;nginx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/user-agents"&gt;user-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gunicorn"&gt;gunicorn&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="logging"/><category term="nginx"/><category term="user-agents"/><category term="heroku"/><category term="gunicorn"/></entry><entry><title>System Administration: What service/product do you recommend for central logging of events and errors from multiple servers? Why?</title><link href="https://simonwillison.net/2012/Feb/15/system-administration-what-serviceproduct/#atom-tag" rel="alternate"/><published>2012-02-15T18:26:00+00:00</published><updated>2012-02-15T18:26:00+00:00</updated><id>https://simonwillison.net/2012/Feb/15/system-administration-what-serviceproduct/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/System-Administration-What-service-product-do-you-recommend-for-central-logging-of-events-and-errors-from-multiple-servers-Why/answer/Simon-Willison"&gt;System Administration: What service/product do you recommend for central logging of events and errors from multiple servers? Why?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We rolled our own solution to this using MongoDB, due to its super-fast writes and ability to store, index and search JSON. We were also attracted by its capped collections, which make it easy to e.g. only log the last 100,000 items.&lt;/p&gt;

&lt;p&gt;It hasn't given us any problems, but we also haven't spent the time to build a good UI for it do we aren't getting as much value out of it as we could. That's the disadvantage of rolling your own: you have to build the whole thing.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cloud"&gt;cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sysadmin"&gt;sysadmin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cloud"/><category term="logging"/><category term="php"/><category term="sysadmin"/><category term="quora"/></entry><entry><title>clarity</title><link href="https://simonwillison.net/2009/Nov/4/clarity/#atom-tag" rel="alternate"/><published>2009-11-04T22:36:04+00:00</published><updated>2009-11-04T22:36:04+00:00</updated><id>https://simonwillison.net/2009/Nov/4/clarity/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://github.com/tobi/clarity/"&gt;clarity&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A web interface for tailing and grepping the log files in /var/log, written in Ruby and EventMachine.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/clarity"&gt;clarity&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/eventmachine"&gt;eventmachine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;&lt;/p&gt;



</summary><category term="clarity"/><category term="eventmachine"/><category term="logging"/><category term="ruby"/></entry><entry><title>Python Logging 101</title><link href="https://simonwillison.net/2009/Sep/29/logging/#atom-tag" rel="alternate"/><published>2009-09-29T18:40:33+00:00</published><updated>2009-09-29T18:40:33+00:00</updated><id>https://simonwillison.net/2009/Sep/29/logging/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://plumberjack.blogspot.com/2009/09/python-logging-101.html"&gt;Python Logging 101&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A really useful introduction to Python’s logging module by that module’s author, Vinay Sajip.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vinaysajip"&gt;vinaysajip&lt;/a&gt;&lt;/p&gt;



</summary><category term="logging"/><category term="python"/><category term="vinaysajip"/></entry><entry><title>Django ponies: Proposals for Django 1.2</title><link href="https://simonwillison.net/2009/Sep/28/ponies/#atom-tag" rel="alternate"/><published>2009-09-28T23:32:04+00:00</published><updated>2009-09-28T23:32:04+00:00</updated><id>https://simonwillison.net/2009/Sep/28/ponies/#atom-tag</id><summary type="html">
    &lt;p&gt;I've decided to step up my involvement in Django development in the run-up to Django 1.2, so I'm currently going through several years worth of accumulated pony requests figuring out which ones are worth advocating for. I'm also ensuring I have the code to back them up - my innocent &lt;a href="http://code.djangoproject.com/wiki/AutoEscaping"&gt;AutoEscaping proposal&lt;/a&gt; a few years ago resulted in an enormous amount of work by Malcolm and I don't think he'd appreciate a repeat performance.&lt;/p&gt;

&lt;p&gt;I'm not a big fan of branches when it comes to exploratory development - they're fine for doing the final implementation once an approach has been agreed, but I don't think they are a very effective way of discussing proposals. I'd much rather see working code in a separate application - that way I can try it out with an existing project without needing to switch to a new Django branch. Keeping code out of a branch also means people can start using it for real development work, making the API much easier to evaluate. Most of my proposals here have accompanying applications on GitHub.&lt;/p&gt;

&lt;p&gt;I've recently got in to the habit of including an "examples" directory with each of my experimental applications. This is a full Django project (with settings.py, urls.py and manage.py files) which serves two purposes. Firstly, it allows developers to run the application's unit tests without needing to install it in to their own pre-configured project, simply by changing in to the examples directory and running &lt;samp&gt;./manage.py test&lt;/samp&gt;. Secondly, it gives me somewhere to put demonstration code that can be viewed in a browser using the runserver command - a further way of making the code easier to evaluate. &lt;a href="http://github.com/simonw/django-safeform"&gt;django-safeform&lt;/a&gt; is a good example of this pattern.&lt;/p&gt;

&lt;p&gt;Here's my current list of ponies, in rough order of priority.&lt;/p&gt;

&lt;h4&gt;Signing and signed cookies&lt;/h4&gt;

&lt;p&gt;Signing strings to ensure they have not yet been tampered with is a crucial technique in web application security. As with all cryptography, it's also surprisingly difficult to do correctly. &lt;a href="http://vnhacker.blogspot.com/2009/09/flickrs-api-signature-forgery.html"&gt;A vulnerability in the signing implementation&lt;/a&gt; used to protect the Flickr API was revealed just today.&lt;/p&gt;

&lt;p&gt;One of the many uses of signed strings is to implement signed cookies. Signed cookies are fantastically powerful - they allow you to send cookies safe in the knowledge that your user will not be able to alter them without you knowing. This dramatically reduces the need for sessions - most web apps use sessions for security rather than for storing large amounts of data, so moving that "logged in user ID" value to a signed cookie eliminates the need for session storage entirely, saving a round-trip to persistent storage on every request.&lt;/p&gt;

&lt;p&gt;This has particularly useful implications for scaling - you can push your shared secret out to all of your front end web servers and scale horizontally, with no need for shared session storage just to handle simple authentication and "You are logged in as X" messages.&lt;/p&gt;

&lt;p&gt;The latest version of my &lt;a href="http://github.com/simonw/django-openid"&gt;django-openid&lt;/a&gt; library uses signed cookies to store the OpenID you log in with, removing the need to configure Django's session storage. I've extracted that code in to &lt;a href="http://github.com/simonw/django-signed"&gt;django-signed&lt;/a&gt;, which I hope to evolve in to something suitable for inclusion in &lt;samp&gt;django.utils&lt;/samp&gt;.&lt;/p&gt;

&lt;p&gt;Please note that django-signed has not yet been vetted by cryptography specialists, something I plan to fix before proposing it for final inclusion in core.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="http://github.com/simonw/django-signed"&gt;django-signed&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://code.djangoproject.com/wiki/Signing"&gt;Details of the Signing proposal&lt;/a&gt; on the Django wiki&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/133509246caf1d91"&gt;Signing discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Improved CSRF support&lt;/h4&gt;

&lt;p&gt;This is mainly Luke Plant's pony, but I'm very keen to see it happen. Django has shipped with CSRF protection for &lt;a href="http://code.djangoproject.com/changeset/2868"&gt;more than three years now&lt;/a&gt;, but the approach (using middleware to rewrite form HTML) is relatively crude and, crucially, the protection isn't turned on by default. Hint: if you aren't 100% positive you are protected against &lt;a href="http://en.wikipedia.org/wiki/Cross-site_request_forgery"&gt;CSRF&lt;/a&gt;, you should probably go and turn it on.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://bitbucket.org/spookylukey/django-trunk-lukeplant/src/05f0530f3207/django/contrib/csrf/"&gt;Luke's approach&lt;/a&gt; is an iterative improvement - a template tag (with a dependency on RequestContext) is used to output the hidden CSRF field, with middleware used to set the cookie and perform the extra validation. I experimented at length with an alternative solution based around extending Django's form framework to treat CSRF as just another aspect of validation - you can see the result in my &lt;a href="http://github.com/simonw/django-safeform"&gt;django-safeform&lt;/a&gt; project. My approach avoids middleware and template tags in favour of a view decorator to set the cookie and a class decorator to add a CSRF check to the form itself.&lt;/p&gt;

&lt;p&gt;While my approach works, the effort involved in upgrading existing code to it is substantial, compared to a much easier upgrade path for Luke's middleware + template tag approach. The biggest advantage of safeform is that it allows CSRF failure messages to be shown inline on the form, without losing the user's submission - the middleware check means showing errors as a full page without redisplaying the form. It looks like it should be possible to bring that aspect of safeform back to the middleware approach, and I plan to put together a patch for that over the next few days.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;Luke's &lt;a href="http://bitbucket.org/spookylukey/django-trunk-lukeplant/src/05f0530f3207/django/contrib/csrf/"&gt;CSRF branch&lt;/a&gt; on bitbucket&lt;/li&gt;
    &lt;li&gt;My &lt;a href="http://github.com/simonw/django-signed"&gt;django-safeform&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://code.djangoproject.com/wiki/CsrfProtection"&gt;Details of the CSRF proposal&lt;/a&gt; on the Django wiki&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/3d2dc750082103dc"&gt;CSRF discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Better support for outputting HTML&lt;/h4&gt;

&lt;p&gt;This is a major pet peeve of mine. Django's form framework is excellent - one of the best features of the framework. There's just one thing that bugs me about it - it outputs full form widgets (for &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt; and the like) so that it can include the previous value when redisplaying a form during validation, but it does so using XHTML syntax.&lt;/p&gt;

&lt;p&gt;I have a strong preference for an HTML 4.01 strict doctype, and all those &amp;lt;self-closing-tags /&amp;gt; have been niggling away at me for literally &lt;em&gt;years&lt;/em&gt;. Django bills itself as a framework for "perfectionists with deadlines", so I feel justified in getting wound up out of proportion over this one.&lt;/p&gt;

&lt;p&gt;A year ago I started experimenting with a solution, and came up with &lt;a href="http://github.com/simonw/django-html"&gt;django-html&lt;/a&gt;. It introduces two new Django template tags - &lt;code&gt;{% doctype %}&lt;/code&gt; and &lt;code&gt;{% field %}&lt;/code&gt;. The doctype tag serves two purposes - it outputs a particular doctype (saving you from having to remember the syntax) and it records that doctype in Django's template context object. The field tag is then used to output form fields, but crucially it gets to take the current doctype in to account.&lt;/p&gt;

&lt;p&gt;The field tag can also be used to add extra HTML attributes to form widgets from within the template itself, solving another small frustration about the existing form library. The &lt;a href="http://github.com/simonw/django-html/blob/master/README.rst"&gt;README&lt;/a&gt; describes the new tags in detail.&lt;/p&gt;

&lt;p&gt;The way the tags work is currently a bit of a hack - if merged in to Django core they could be more cleanly implemented by refactoring the form library slightly. This refactoring is currently being discussed on the mailing list.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="http://github.com/simonw/django-html"&gt;django-html&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/bbf75f0eeaf9fa64"&gt;Improved HTML discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Logging&lt;/h4&gt;

&lt;p&gt;This is the only proposal for which I don't yet have any code. I want to add official support for Python's standard logging framework to Django. It's possible to use this at the moment (I've done so on several projects) but it's not at all clear what the best way of doing so is, and Django doesn't use it internally at all. I posted a &lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/8551ecdb7412ab22"&gt;full argument in favour of logging&lt;/a&gt; to the mailing list, but my favourite argument is this one:&lt;/p&gt;

&lt;blockquote cite="http://groups.google.com/group/django-developers/browse_thread/thread/8551ecdb7412ab22"&gt;&lt;p&gt;Built-in support for logging reflects a growing reality of modern Web development: more and more sites have interfaces with external web service APIs, meaning there are plenty of things that could go wrong that are outside the control of the developer. Failing gracefully and logging what happened is the best way to deal with 3rd party problems - much better than throwing a 500 and leaving no record of what went wrong.&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;I'm not actively pursuing this one yet, but I'm very interesting in hearing people's opinions on the best way to configure and use the Python logging module in production.&lt;/p&gt;

&lt;h4&gt;A replacement for get_absolute_url()&lt;/h4&gt;

&lt;p&gt;Django has a loose convention of encouraging people to add a &lt;code&gt;get_absolute_url&lt;/code&gt; method to their models that returns that object's URL. It's a controversial feature - for one thing, it's a bit of a layering violation since URL logic is meant to live in the &lt;samp&gt;urls.py&lt;/samp&gt; file. It's incredibly convenient though, and since it's good web citizenship for everything to have one and only one URL I think there's a pretty good argument for keeping it.&lt;/p&gt;

&lt;p&gt;The problem is, the name sucks. I first took a look at this in the last few weeks before the release of Django 1.0 - what started as a quick proposal to come up with a better name before we were stuck with it quickly descended in to a quagmire as I realised quite how broken &lt;code&gt;get_absolute_url()&lt;/code&gt; is. The short version: in some cases it means "get a relative URL starting with /", in other cases it means "get a full URL starting with http://" and the name doesn't accurately describe either.&lt;/p&gt;

&lt;p&gt;A full write-up of my investigation is &lt;a href="http://code.djangoproject.com/wiki/ReplacingGetAbsoluteUrl"&gt;available on the Wiki&lt;/a&gt;. My proposed solution was to replace it with two complementary methods - &lt;code&gt;get_url()&lt;/code&gt; and &lt;code&gt;get_url_path()&lt;/code&gt; - with the user implementing one hence allowing the other one to be automatically derived. My &lt;a href="http://github.com/simonw/django-urls"&gt;django-urls&lt;/a&gt; project illustrates the concept via a model mixin class. A year on I still think it's quite a neat idea, though as far as I can tell no one has ever actually used it.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="http://code.djangoproject.com/wiki/ReplacingGetAbsoluteUrl"&gt;ReplacingGetAbsoluteUrl&lt;/a&gt; on the wiki&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://github.com/simonw/django-urls"&gt;django-urls&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/7e69c39c23ec1079"&gt;Recent get_absolute_url discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comments on this post are open, but if you have anything to say about any of the individual proposals it would be much more useful if you posted it to the relevant mailing list thread.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cryptography"&gt;cryptography&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/luke-plant"&gt;luke-plant&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markup"&gt;markup&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ponies"&gt;ponies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/signedcookies"&gt;signedcookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/signing"&gt;signing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xhtml"&gt;xhtml&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cookies"/><category term="cryptography"/><category term="csrf"/><category term="django"/><category term="html"/><category term="logging"/><category term="luke-plant"/><category term="markup"/><category term="ponies"/><category term="projects"/><category term="python"/><category term="security"/><category term="signedcookies"/><category term="signing"/><category term="xhtml"/></entry><entry><title>Justniffer</title><link href="https://simonwillison.net/2009/Sep/25/justniffer/#atom-tag" rel="alternate"/><published>2009-09-25T22:12:51+00:00</published><updated>2009-09-25T22:12:51+00:00</updated><id>https://simonwillison.net/2009/Sep/25/justniffer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://justniffer.sourceforge.net/"&gt;Justniffer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Packet sniffing tool that can output sniffed HTTP traffic formatted the same way as an Apache access_log file.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apache"&gt;apache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/justniffer"&gt;justniffer&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/networking"&gt;networking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packetsniffing"&gt;packetsniffing&lt;/a&gt;&lt;/p&gt;



</summary><category term="apache"/><category term="justniffer"/><category term="logging"/><category term="networking"/><category term="packetsniffing"/></entry><entry><title>"MongoDB is fantastic for logging"</title><link href="https://simonwillison.net/2009/Aug/26/logging/#atom-tag" rel="alternate"/><published>2009-08-26T19:09:26+00:00</published><updated>2009-08-26T19:09:26+00:00</updated><id>https://simonwillison.net/2009/Aug/26/logging/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.mongodb.org/post/172254834/mongodb-is-fantastic-for-logging"&gt;&amp;quot;MongoDB is fantastic for logging&amp;quot;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Sounds tempting... high performance inserts, JSON structured records and capped collections if you only want to keep the past X entries. If you care about older historic data but still want to preserve space you could run periodic jobs to roll up log entries in to summarised records. It shouldn’t be too hard to write a command-line script that hooks in to Apache’s logging directive and writes records to MongoDB.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apache"&gt;apache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mongodb"&gt;mongodb&lt;/a&gt;&lt;/p&gt;



</summary><category term="apache"/><category term="json"/><category term="logging"/><category term="mongodb"/></entry><entry><title>Python logging from multiple processes</title><link href="https://simonwillison.net/2009/Aug/13/python/#atom-tag" rel="alternate"/><published>2009-08-13T23:55:02+00:00</published><updated>2009-08-13T23:55:02+00:00</updated><id>https://simonwillison.net/2009/Aug/13/python/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.huyng.com/archives/418"&gt;Python logging from multiple processes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Use Python’s socket log handler to send all log messages to a single server—the python-loggingserver project implements such a server as a Twisted application with a handy web interface for viewing the aggregated logs.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twisted"&gt;twisted&lt;/a&gt;&lt;/p&gt;



</summary><category term="logging"/><category term="python"/><category term="twisted"/></entry><entry><title>django-db-log</title><link href="https://simonwillison.net/2008/May/13/djangodblog/#atom-tag" rel="alternate"/><published>2008-05-13T08:07:49+00:00</published><updated>2008-05-13T08:07:49+00:00</updated><id>https://simonwillison.net/2008/May/13/djangodblog/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.davidcramer.net/code/126/django-db-log.html"&gt;django-db-log&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Middleware that logs Django exceptions to the database, using a clever scheme based on an MD5 of the traceback text to group duplicate errors in to batches.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/david-cramer"&gt;david-cramer&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djangodblog"&gt;djangodblog&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/exceptions"&gt;exceptions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/middleware"&gt;middleware&lt;/a&gt;&lt;/p&gt;



</summary><category term="david-cramer"/><category term="django"/><category term="djangodblog"/><category term="exceptions"/><category term="logging"/><category term="middleware"/></entry><entry><title>daemon.py</title><link href="https://simonwillison.net/2008/Jan/8/daemon/#atom-tag" rel="alternate"/><published>2008-01-08T21:58:54+00:00</published><updated>2008-01-08T21:58:54+00:00</updated><id>https://simonwillison.net/2008/Jan/8/daemon/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://hathawaymix.org/Software/Sketches/daemon.py"&gt;daemon.py&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat little Python module for daemonizing a process; handles logging and pid files out of the box.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://blog.ianbicking.org/daemon-best-practices.html"&gt;Daemon Best Practices&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/daemon"&gt;daemon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/daemonizing"&gt;daemonizing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pid"&gt;pid&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="daemon"/><category term="daemonizing"/><category term="logging"/><category term="pid"/><category term="python"/></entry><entry><title>LoggerFS</title><link href="https://simonwillison.net/2007/Oct/29/introduction/#atom-tag" rel="alternate"/><published>2007-10-29T10:40:41+00:00</published><updated>2007-10-29T10:40:41+00:00</updated><id>https://simonwillison.net/2007/Oct/29/introduction/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://portal.itauth.com/loggerfs-introduction"&gt;LoggerFS&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Clever use of FUSE: a virtual filesystem which looks out for lines appended to a log file (matched with a regular expression) and stores them in a database instead.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/fuse"&gt;fuse&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/loggerfs"&gt;loggerfs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;&lt;/p&gt;



</summary><category term="fuse"/><category term="loggerfs"/><category term="logging"/></entry><entry><title>jQuery Logging</title><link href="https://simonwillison.net/2007/Oct/19/jquery/#atom-tag" rel="alternate"/><published>2007-10-19T12:52:50+00:00</published><updated>2007-10-19T12:52:50+00:00</updated><id>https://simonwillison.net/2007/Oct/19/jquery/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://happygiraffe.net/blog/archives/2007/09/26/jquery-logging"&gt;jQuery Logging&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Brilliant four line jQuery plugin that lets you insert Firebug console.log() calls directly in to chains.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://happygiraffe.net/blog/"&gt;Jabbering Giraffe&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/debugging"&gt;debugging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dominic-mitchell"&gt;dominic-mitchell&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firebug"&gt;firebug&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jquery"&gt;jquery&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;&lt;/p&gt;



</summary><category term="debugging"/><category term="dominic-mitchell"/><category term="firebug"/><category term="javascript"/><category term="jquery"/><category term="logging"/></entry><entry><title>django-logging</title><link href="https://simonwillison.net/2007/Apr/24/djangologging/#atom-tag" rel="alternate"/><published>2007-04-24T06:50:43+00:00</published><updated>2007-04-24T06:50:43+00:00</updated><id>https://simonwillison.net/2007/Apr/24/djangologging/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://code.google.com/p/django-logging/"&gt;django-logging&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Looks neat—includes the ability to use Python’s standard logging module to log messages to a footer appended to your site’s HTML output.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="logging"/><category term="python"/></entry></feed>