Installing Seq as a Logger in a Golang Project

Husni Nur Fadillah
Husni Nur Fadillah 4 min read
Installing Seq as a Logger in a Golang Project

Background

Simply put, a logger is a tool used to record or store information about activities generated by a program or system that has been equipped with a logger.

"Why install a logger?"

I'll draw from my own experience. Once, an application running in production suddenly crashed. Fortunately, the application already had a logger installed. When the crash occurred, a notification was immediately sent to the company's Slack channel. This allowed developers to notice and fix the issue quickly. During development, when QA finds a bug, it can be tedious and slow if we constantly ask for the payload that caused the bug or request QA to record a screen to show how to reproduce it (though there are cases where we need to understand how to reproduce it from the user's perspective).

Here are other reasons to use logging:

  1. Application Monitoring
  2. Application Performance Analysis
  3. Bug Tracking
  4. Audit and Security
  5. Documentation

Installing Seq

For this article, I'm using a Linux environment. According to the documentation, Seq doesn't provide a native Linux build, but they do provide a Docker image. So I'll use Docker to install Seq locally.

PH=$(echo '<password>' | docker run --rm -i datalust/seq config hash)
  • Replace <password> with the password you want to hash
  • This runs a Docker container with the datalust/seq image in interactive mode to generate a hash of the password used for the 'admin' account in Seq
  • Docker will automatically download the datalust/seq image if it doesn't already exist locally
  • The hashed password is stored in the PH variable

Next, run the following command to start a Docker container using the datalust/seq image:

 
docker run \
  --name seq \
  -d \
  --restart unless-stopped \
  -e ACCEPT_EULA=Y \
  -e SEQ_API_CANONICALURI=https://seq.example.com \
  -e SEQ_FIRSTRUN_ADMINPASSWORDHASH="$PH" \
  -v /path/to/seq/data:/data \
  -p 80:80 \
  -p 5341:5341 \
  datalust/seq

After successfully running the above command, check the Docker container status with docker ps. If it fails, you can check the error logs with docker logs.

Open the Seq UI at http://localhost:80, then enter the username and password (before hashing) that you used when running the Docker container.

Go Seq Logger

I'm using the Fiber framework and a flat project structure for easier understanding. For the complete code, see the GitHub repository link. Source Code

 
Dependencies

 

Implementation

Create a NewLogger function to initialize the logger by adding a Seq hook to the logger instance, so every log message from that instance will be sent to the Seq service. I'm using the singleton design pattern here because I want to ensure the logger instance is created and configured only once.

var (
 logger     *logrus.Logger
 loggerInit sync.Once
)

func LogrusGetLevel(logLevel string) logrus.Level {
 switch strings.ToLower(logLevel) {
 case "fatal":
  return logrus.FatalLevel
 case "error":
  return logrus.ErrorLevel
 case "warn":
  return logrus.WarnLevel
 case "info":
  return logrus.InfoLevel
 case "debug":
  return logrus.DebugLevel
 case "trace":
  return logrus.TraceLevel
 }
 return logrus.InfoLevel
}

func NewLogger() *logrus.Logger {
 loggerInit.Do(func() {
  logger = logrus.New()
  logger.SetFormatter(&easy.Formatter{
   TimestampFormat: FullTimeFormat,
   LogFormat:       fmt.Sprintf("%s\n", `[%lvl%]: "%time%" %msg%`),
  })
  logger.SetLevel(LogrusGetLevel("debug"))
  logger.AddHook(logruseq.NewSeqHook("http://localhost:5341"))
 })

 return logger
}

The following HTTP handler sends an info log level with path data and the response:

 log := NewLogger()

 app.Get("/", func(c *fiber.Ctx) error {
  data := "Yahallo, World!"
  fmt.Println(c.Request().URI())
  log.Infof("Info | %s | %s", c.Request().URI().String(), data)
  return c.SendString(data)
 })
 
 When the API is called, the log message will be sent to Seq.

Customizing Log Messages

I wanted log messages to contain information such as when the endpoint was hit, which endpoint URL was accessed, the request body sent by the client, the response received by the client, the duration of the endpoint call, message, and project name. 

func CreateLog(c *fiber.Ctx, log *logrus.Logger, code int, message string, respData ResponseData) {
 reqBody := string(c.Request().Body())
 path := c.Request().URI().String()

 if code == fiber.StatusOK || code == fiber.StatusAccepted || code == fiber.StatusCreated {
  log.WithFields(logrus.Fields{
   "At":            time.Now(),
   "Method":        c.Method(),
   "Path":          path,
   "ParamRequest":  reqBody,
   "ParamResponse": respData,
   "Message":       message,
   "Duration":      time.Since(time.Now()),
   "Project":       projectName,
  }).Info(c.Method() + " " + path)
 } else if code == fiber.StatusBadRequest || code == fiber.StatusConflict || code == fiber.StatusUnauthorized || code == fiber.StatusNotFound {
  log.WithFields(logrus.Fields{
   "At":            time.Now(),
   "Method":        c.Method(),
   "Path":          path,
   "ParamRequest":  reqBody,
   "ParamResponse": respData,
   "Message":       message,
   "Duration":      time.Since(time.Now()),
   "Project":       projectName,
  }).Warn(c.Method() + " " + path)
 } else {
  log.WithFields(logrus.Fields{
   "At":            time.Now(),
   "Method":        c.Method(),
   "Path":          path,
   "ParamRequest":  reqBody,
   "ParamResponse": respData,
   "Message":       message,
   "Duration":      time.Since(time.Now()),
   "Project":       projectName,
  }).Error(c.Method() + " " + path)
 }
}
 
func Response_Log(ctx *fiber.Ctx, log *logrus.Logger, code int, message string, data interface{}) error {
 RespData := ResponseData{
  Data: data,
  Status: StatusResponseData{
   Code:    code,
   Message: message,
  },
  TimeStamp: GenerateTimeJakarta(),
 }

 CreateLog(ctx, log, code, message, RespData)
 return ctx.Status(code).JSON(RespData)
}

Implementation in HTTP handlers:

 app.Get("/yahallos", func(c *fiber.Ctx) error {

  data := []string{
   "yahallo 1",
   "yahallo 2",
   "yahallo 3",
  }

  return Response_Log(c, log, fiber.StatusOK, "Success to get all yahallos", data)
 })

 app.Post("/say-yahallo", func(c *fiber.Ctx) error {
  var payload SayYahalloReq

  err := c.BodyParser(&payload)
  if err != nil {
   return Response_Log(c, log, fiber.StatusBadRequest, "fail to parse request body", nil)
  }

  data := fmt.Sprintf("%s (yahallo) %s", payload.Hello, payload.Name)

  return Response_Log(c, log, fiber.StatusOK, "Success to say hello", data)
 })

Output from the customized log message:

Closing

That concludes the article "Installing Seq as a Logger in a Golang Project."  I hope you enjoyed reading this article and found what you were looking for.

 

 

Share this post