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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

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:
1
2
3
4
5
6
7

	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.