ApplicationInsights-Go icon indicating copy to clipboard operation
ApplicationInsights-Go copied to clipboard

Standard logger interface

Open Magicking opened this issue 6 years ago • 5 comments

Using a standard logger interface would allow having a drop-in replacement for Go logging with ApplicationInsights

Magicking avatar Feb 26 '19 12:02 Magicking

Do you have any suggestions? I know of log in the standard library, but am unaware of any widely-used frameworks out there.

jjjordanmsft avatar Mar 04 '19 20:03 jjjordanmsft

Sure, a drop-in replacement for log with all of log.* functions implemented would be a good start but what would be best I think is something like https://github.com/sirupsen/logrus .

Especially log.WithFields(log.Fields{}) as it would fit best what App Insight UI shows.

Magicking avatar Mar 04 '19 20:03 Magicking

hi @Magicking

I experimented this night with a simple implementation of application insights for our services. Maybe this code snipets help you. I wrote some global handler for the frameworks logrus, gin and resty, resulting in quite a great out-of-the-box experience

cheers

config.go

package insights

import "github.com/Microsoft/ApplicationInsights-Go/appinsights"

type InsightsConfig struct {
	InstrumentationKey string

	Role    string
	Version string
}

func createTelemetryClient(config *InsightsConfig) appinsights.TelemetryClient {
	client := appinsights.NewTelemetryClient(config.InstrumentationKey)

	if len(config.Role) > 0 {
		client.Context().Tags.Cloud().SetRole(config.Role)
	}

	if len(config.Version) > 0 {
		client.Context().Tags.Application().SetVer(config.Version)
	}

	return client
}

logrus.go

To log normal application logging, we do something like this:

package insights

import (
	"encoding/json"
	"fmt"

	"github.com/Microsoft/ApplicationInsights-Go/appinsights"
	"github.com/Microsoft/ApplicationInsights-Go/appinsights/contracts"
	"github.com/sirupsen/logrus"
)

func SetupLogrus(config *InsightsConfig) {
	hook := &LogrusHook{
		Client: createTelemetryClient(config),
	}

	logrus.AddHook(hook)
}

type LogrusHook struct {
	Client appinsights.TelemetryClient
}

func (hook *LogrusHook) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (hook *LogrusHook) Fire(entry *logrus.Entry) error {
	if _, ok := entry.Data["message"]; !ok {
		entry.Data["message"] = entry.Message
	}

	level := convertSeverityLevel(entry.Level)
	telemetry := appinsights.NewTraceTelemetry(entry.Message, level)

	for key, value := range entry.Data {
		value = formatData(value)
		telemetry.Properties[key] = fmt.Sprintf("%v", value)
	}

	hook.Client.Track(telemetry)
	return nil
}

func convertSeverityLevel(level logrus.Level) contracts.SeverityLevel {
	switch level {
	case logrus.PanicLevel:
		return contracts.Critical
	case logrus.FatalLevel:
		return contracts.Critical
	case logrus.ErrorLevel:
		return contracts.Error
	case logrus.WarnLevel:
		return contracts.Warning
	case logrus.InfoLevel:
		return contracts.Information
	case logrus.DebugLevel, logrus.TraceLevel:
		return contracts.Verbose
	default:
		return contracts.Information
	}
}

func formatData(value interface{}) (formatted interface{}) {
	switch value := value.(type) {
	case json.Marshaler:
		return value
	case error:
		return value.Error()
	case fmt.Stringer:
		return value.String()
	default:
		return value
	}
}

gin

to log incoming requsts, we wrote a simple middleware for gin

package insights

import (
	"strconv"
	"time"

	"github.com/Microsoft/ApplicationInsights-Go/appinsights"
	"github.com/gin-gonic/gin"
)

func SetupGin(engine *gin.Engine, config *InsightsConfig) {
	engine.Use(InsightsWithConfig(config))
}

func InsightsWithConfig(config *InsightsConfig) gin.HandlerFunc {
	client := createTelemetryClient(config)

	return func(c *gin.Context) {
		url := c.Request.RequestURI

		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery

		// Process request
		c.Next()

		if path == "/readiness" || path == "/liveness" {
			return
		}

		end := time.Now()
		duration := end.Sub(start)

		clientIP := c.ClientIP()
		method := c.Request.Method
		statusCode := c.Writer.Status()
		userAgent := c.Request.UserAgent()
		//errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
		bodySize := c.Writer.Size()

		if query != "" {
			path = path + "?" + query
		}

		telemetry := appinsights.NewRequestTelemetry(method, url, duration, strconv.Itoa(statusCode))

		telemetry.Timestamp = end
		telemetry.MarkTime(start, end)

		telemetry.Source = clientIP
		telemetry.Success = statusCode >= 200 && statusCode < 400

		telemetry.Properties["user-agent"] = userAgent
		telemetry.Measurements["body-size"] = float64(bodySize)

		client.Track(telemetry)
	}
}

resty

we use resty as rest client for calling other microservices or external services.


package insights

import (
	"fmt"
	"net/url"
	"strconv"

	"github.com/Microsoft/ApplicationInsights-Go/appinsights"
	"gopkg.in/resty.v1"
)

func SetupResty(config *InsightsConfig) {
	resty.OnAfterResponse(restyHandler(config))
}

func restyHandler(config *InsightsConfig) func(*resty.Client, *resty.Response) error {
	client := createTelemetryClient(config)

	return func(c *resty.Client, response *resty.Response) error {
		request := response.Request

		start := request.Time
		duration := response.Time()
		statusCode := response.StatusCode()

		name := fmt.Sprintf("%s %s", request.Method, request.URL)
		success := statusCode < 400 || statusCode == 401
		target := request.RawRequest.Host

		// Sanitize URL for the request name
		if parsedUrl, err := url.Parse(request.URL); err == nil {
			// Remove the query
			parsedUrl.RawQuery = ""
			parsedUrl.ForceQuery = false

			// Remove the fragment
			parsedUrl.Fragment = ""

			// Remove the user info, if any.
			parsedUrl.User = nil

			// Write back to name
			name = parsedUrl.String()
		}

		name = fmt.Sprintf("%s %s", request.Method, name)

		telemetry := appinsights.NewRemoteDependencyTelemetry(name, "HTTP", target, success)
		telemetry.Data = request.URL
		telemetry.ResultCode = strconv.Itoa(statusCode)
		telemetry.MarkTime(start, start.Add(duration))

		client.Track(telemetry)

		return nil
	}
}

your main


if key, ok := os.LookupEnv("AZURE_INSTRUMENTATION_KEY"); ok {
		insightsConfig := &insights.InsightsConfig{
			Role:    NAMEOFYOURAPP,
			Version:  VERSIONOFYOURAPP,

			InstrumentationKey: key,
		}

		insights.SetupGin(router, insightsConfig) // remove if not using gin
		insights.SetupResty(insightsConfig)  // remove if not using resty
		insights.SetupLogrus(insightsConfig)  // remove if not using logrus
	}

adrianliechti avatar Mar 12 '19 10:03 adrianliechti

With the following middleware, we get empty Operation name in Azure application insight Performance menu. What am I missing here, any suggestion is much appreciated.

func InsightsWithConfig(config *InsightsConfig) gin.HandlerFunc {
	client := createTelemetryClient(config)
	return func(c *gin.Context) {
		url := c.Request.RequestURI
		start := time.Now()
		c.Next()
		end := time.Now()
		duration := end.Sub(start)
		clientIP := c.ClientIP()
		method := c.Request.Method
		statusCode := c.Writer.Status()
		userAgent := c.Request.UserAgent()
		bodySize := c.Writer.Size()
		telemetry := appinsights.NewRequestTelemetry(method, url, duration, strconv.Itoa(statusCode))
		telemetry.Timestamp = end
		telemetry.MarkTime(start, end)
		telemetry.Source = clientIP
		telemetry.Success = statusCode >= 200 && statusCode < 400
		telemetry.Properties["user-agent"] = userAgent
		telemetry.Measurements["body-size"] = float64(bodySize)
		client.Track(telemetry)
	}

probal avatar Nov 02 '21 13:11 probal

@probal I figure out the solution that you have to specify the operation name through telemetry tag in this middleware as well like

telemetry.Tags[contracts.OperationName] = fmt.Sprintf("%s %s", method, c.Request.URL.Path)

zhengyuan-ehps avatar Oct 31 '23 19:10 zhengyuan-ehps