Skip to content

Instantly share code, notes, and snippets.

@ChristopherDavenport
Created December 11, 2021 19:27
Show Gist options
  • Save ChristopherDavenport/90e528be71f505561894d6e8727084a9 to your computer and use it in GitHub Desktop.
Save ChristopherDavenport/90e528be71f505561894d6e8727084a9 to your computer and use it in GitHub Desktop.
NCSE Common Log - Access Log
package io.chrisdavenport.http4s.logging
import cats._
import cats.syntax.all._
import cats.data._
import cats.effect._
import cats.effect.syntax.all._
import org.http4s._
import com.comcast.ip4s._
import java.time.format.DateTimeFormatter
import java.time.ZoneId
/*
127.0.0.1 user-identifier john [20/Jan/2020:21:32:14 -0700] "GET /apache_pb.gif HTTP/1.0" 200 4782
‍Here is an explanation of what every part of this code means:
- 127.0.0.1 - refers to the IP address of the client (the remote host) that made the request to the server.
- user-identifier is the Ident protocol (also known as Identification Protocol, or Ident) of the client.
- john is the userid (user identification) of the person that is requesting the document.
- [20/Jan/2020:21:32:14 -0700] - is the date, time, and time zone that logs when the request was attempted.
By default, it is in the strftime format of %d/%b/%Y:%H:%M:%S %z.
- "GET /apache_pb.gif HTTP/1.0" is the client’s request line. GET refers to the method,
- apache_pb.gif is the resource that was requested, and HTTP/1.0 is the HTTP protocol.
- 200 is the HTTP status code that was returned to the client after the request. 2xx is a successful response, 3xx is a redirection, 4xx is a client error, and 5xx is a server error.
- 4782 is the size of the object - measured in bytes - that was returned to the client in question.
*/
object NCSECommonLog {
def apply[F[_]: Temporal, G[_]](logAction: String => F[Unit], onCancel: (Status, Option[Long]), onError: (Status, Option[Long]), http: Http[F, G]): Http[F, G] = {
def log(ncseData: NCSEData): F[Unit] = logAction(ncseData.renderString)
Kleisli{ (req: Request[G]) =>
http.run(req).guaranteeCase{
case Outcome.Succeeded(fa) =>
fa.flatMap{resp =>
NCSEData.getDateString[F].flatMap{dateString =>
val data = NCSEData.create(req, dateString, resp.status, resp.contentLength)
log(data)
}
}
case Outcome.Errored(_) =>
NCSEData.getDateString[F].flatMap{dateString =>
val data = NCSEData.create(req, dateString, onError._1, onError._2)
log(data)
}
case Outcome.Canceled() =>
NCSEData.getDateString[F].flatMap{dateString =>
val data = NCSEData.create(req, dateString, onCancel._1, onCancel._2)
log(data)
}
}
}
}
private[logging] case class NCSEData(
remoteHost: Option[SocketAddress[IpAddress]],
// identification: Option[String], // TODO figure this out
userId: Option[String],
date: String, // Epoch Time, second precision
requestPrelude: String,
respStatus: Status,
respLength: Option[Long],
){
def renderString = NCSEData.renderString(this)
}
private[logging] object NCSEData {
private val dateTimeFormat = DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z")
private def printTime[F[_]: Concurrent](i: java.time.Instant): F[String] = Applicative[F].unit.map{_ =>
val zone = ZoneId.systemDefault()
val zdt = i.atZone(zone)
zdt.format(dateTimeFormat)
}
def getDateString[F[_]: Temporal]: F[String] = Temporal[F].realTimeInstant.flatMap(printTime[F])
private val LSB = "["
private val RSB = "]"
private val DASH = "-"
private val SPACE = " "
private val DQUOTE = "\""
def renderString(data: NCSEData): String = {
val sb = new StringBuilder()
sb.append(data.remoteHost.fold(NCSEData.DASH)(sa => sa.host.toString())) // Remote
sb.append(NCSEData.SPACE)
sb.append(NCSEData.DASH) // Ident Protocol Not Implemented
sb.append(NCSEData.SPACE)
sb.append(data.userId.fold(NCSEData.DASH)(identity)) // User
sb.append(NCSEData.SPACE)
sb.append(NCSEData.LSB)
sb.append(data.date)
sb.append(NCSEData.RSB)
sb.append(NCSEData.SPACE)
sb.append(NCSEData.DQUOTE)
sb.append(data.requestPrelude)
sb.append(NCSEData.DQUOTE)
sb.append(NCSEData.SPACE)
sb.append(data.respStatus.code.toString())
sb.append(NCSEData.SPACE)
sb.append(data.respLength.fold(NCSEData.DASH)(_.toString()))
sb.toString()
}
def renderReqPrelude[F[_]](req: Request[F]): String = {
val stringBuilder = new StringBuilder()
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
stringBuilder
.append(req.method.renderString)
.append(SPACE)
.append(req.uri.toOriginForm.renderString)
.append(SPACE)
.append(req.httpVersion.renderString)
stringBuilder.toString()
}
def create[F[_]](req: Request[F], dateString: String, respStatus: Status, respLength: Option[Long]): NCSEData = {
NCSEData(
req.remote,
req.remoteUser,
dateString,
renderReqPrelude(req),
respStatus,
respLength
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment