Created
December 11, 2021 19:27
-
-
Save ChristopherDavenport/90e528be71f505561894d6e8727084a9 to your computer and use it in GitHub Desktop.
NCSE Common Log - Access Log
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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