Skip to content

Instantly share code, notes, and snippets.

@bcardiff
Created July 3, 2023 01:23
Show Gist options
  • Save bcardiff/c65259a082a746033fb2f80d20f0dbf5 to your computer and use it in GitHub Desktop.
Save bcardiff/c65259a082a746033fb2f80d20f0dbf5 to your computer and use it in GitHub Desktop.
Sample Kemal app with per user logging

Build the server

% crystal src/kemal-log.cr
[development] Kemal is ready to lead at http://0.0.0.0:3000

2023-07-03 01:20:01 UTC 200 GET / 240.96µs
[jdoe] Index
2023-07-03 01:20:07 UTC 200 GET /something 71.0µs
[jdoe] Doing something
2023-07-03 01:20:13 UTC 200 GET /something 364.0µs
[rdoe] Doing something

In other terminal

% curl -urdoe:word http://localhost:3000/something
Something done!%                                          
% curl -ujdoe:pass http://localhost:3000/         
Hello World!%                                             
% curl -ujdoe:pass http://localhost:3000/something
Something done!%                                          
% curl -urdoe:word http://localhost:3000/something
Something done!%      
require "log"
require "kemal"
require "kemal-basic-auth"
# A logger that is aware of where the username information is
class PerUserLog < Log::Backend
def write(entry : Log::Entry)
username = entry.context[:username]
# if there is no username just skip it
return unless username
# do something with the log entry
puts "[#{username}] #{entry.message}"
end
end
# setup the logger manually, but we can make it configurable by ENV variables
# using Log.setup or other means if needed.
Log.setup do |c|
c.bind "*", :info, PerUserLog.new
end
# Kemal::BasicAuth from kemalcr/kemal-basic-auth does not offer a nice hook
# but here we duplicate the call method and add a new line to the logger
# alternative we should be able to do a middleware that extract the username
# from the context and add it to the logger context, but I am not sure how to
# enforce the order of the middlewares.
module Kemal::BasicAuth
class Handler
def call(context)
if context.request.headers[AUTH]?
if value = context.request.headers[AUTH]
if value.size > 0 && value.starts_with?(BASIC)
if username = authorize?(value)
context.kemal_authorized_username = username
# new line
Log.context = Log.context.set(username: username)
return call_next(context)
end
end
end
end
headers = HTTP::Headers.new
context.response.status_code = 401
context.response.headers["WWW-Authenticate"] = HEADER_LOGIN_REQUIRED
context.response.print AUTH_MESSAGE
end
end
end
# setup our fake users
basic_auth({"jdoe" => "pass", "rdoe" => "word"})
# business logic! we will have the current user of the request
# in the logger context.
def do_something
Log.info { "Doing something" }
end
# a route that logs directly
get "/" do
Log.info { "Index" }
"Hello World!"
end
# a route that logs indirectly
get "/something" do
do_something
"Something done!"
end
Kemal.run
name: kemal-log
version: 0.1.0
dependencies:
kemal:
github: kemalcr/kemal
kemal-basic-auth:
github: kemalcr/kemal-basic-auth
crystal: 1.8.2
license: MIT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment