Highlight - a Sentry alternative

I have been looking for a platform that I could use for monitoring application health, storing error logs and sending out alerts about exception errors in my application.

Sentry seemed a bit pricey for a bootstrapping software founder trying to keep the lights on.

A quick Google search revealed that there’s actually a service called highlight.io.

What I liked

  • A simple and quick onboarding process.
  • Intuitive design that I did not have to fiddle with.
  • Good enough documentation.
  • A free forever plan for 15 seats.

Setting up logging using Go

Below are quick steps on how you can set up a logging mechanism using Gin.

In my project, all external services live in a services package.

So, all I have to do is create a highlight.go file in the services package.

    package services

    import (
        "os"

        "github.com/highlight/highlight/sdk/highlight-go"
    )

    func InitHighlight() error {
        projectID := os.Getenv("HIGHLIGHT_PROJECT_ID")
        serviceName := os.Getenv("HIGHLIGHT_SERVICE_NAME")
        serviceVersion := os.Getenv("HIGHLIGHT_SERVICE_VERSION")

        highlight.SetProjectID(projectID)
        highlight.Start(
            highlight.WithServiceName(serviceName),
            highlight.WithServiceVersion(serviceVersion),
        )

        return nil
    }

    func StopHighlight() {
        highlight.Stop()
    }

Furthermore, I’m using go.uber.org/zap for logging in production.

Since a logger is a middleware, I have a middleware package in my project.

package middleware

import (
	"encoding/json"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/highlight/highlight/sdk/highlight-go"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

func Logger() gin.HandlerFunc {
	rawJSON := []byte(`{
        "level": "debug",
        "encoding": "json",
        "outputPaths": ["stdout"],
        "errorOutputPaths": ["stderr"],
        "encoderConfig": {
            "messageKey": "message",
            "levelKey": "level",
            "levelEncoder": "lowercase"
        }
    }`)

	cfg := zap.Config{}
	if err := json.Unmarshal(rawJSON, &cfg); err != nil {
		panic(err)
	}

	logger := zap.Must(cfg.Build())
	return func(c *gin.Context) {
		start := time.Now()

		c.Next()

		duration := time.Since(start)

		if len(c.Errors) > 0 {
			for _, err := range c.Errors {
				highlight.RecordError(c.Request.Context(), err.Err,
					attribute.String("method", c.Request.Method),
					attribute.String("path", c.Request.URL.Path),
					attribute.Int("status", c.Writer.Status()),
					attribute.String("userId", c.GetHeader("User-Id")),
					attribute.String("ip", c.ClientIP()),
					attribute.String("user-agent", c.Request.UserAgent()),
				)
			}
		}

		logger.Info("Request",
			zap.String("method", c.Request.Method),
			zap.String("path", c.Request.URL.Path),
			zap.Int("status", c.Writer.Status()),
			zap.Duration("duration", duration),
			zap.String("userId", c.GetHeader("User-Id")),
			zap.String("timestamp", time.Now().Format("2006-01-02 15:04:05")),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
		)
	}
}

For every log, the application’s context is checked to determine whether any errors have occurred so far. If yes, they are recorded and sent to highlight.io using the RecordError method.

if len(c.Errors) > 0 {
			for _, err := range c.Errors {
				highlight.RecordError(c.Request.Context(), err.Err,
					attribute.String("method", c.Request.Method),
					attribute.String("path", c.Request.URL.Path),
					attribute.Int("status", c.Writer.Status()),
					attribute.String("userId", c.GetHeader("User-Id")),
					attribute.String("ip", c.ClientIP()),
					attribute.String("user-agent", c.Request.UserAgent()),
				)
			}
		}

Then in the main method, all that needs to be done is:

  • Attach highlight.io’s Gin middleware to your router:

import (
	highlight "github.com/highlight/highlight/sdk/highlight-go/middleware/gin"
)


func YourRoutes() (*gin.Engine, error) {
	r := gin.Default()
 	r.Use(highlight.Middleware())

return r, nil
}
  • Create an instance of the highlight service in your application’s main package:

	if err := services.InitHighlight(); err != nil {
		slog.Error("Failed to initialize Highlight", "error", err)
		return
	}

	defer services.StopHighlight()

Voila! You have successfully set up monitoring for your application.

Read more about highlight.io.