initial commit

This commit is contained in:
2024-04-26 15:09:54 +02:00
commit 7a6882c09a
10 changed files with 664 additions and 0 deletions

View File

@ -0,0 +1,68 @@
package gelfexporter
type config struct {
GelfUrl string
}
// newConfig creates a validated Config configured with options.
func newConfig(options ...Option) (config, error) {
cfg := config{}
for _, opt := range options {
cfg = opt.apply(cfg)
}
return cfg, nil
}
// Option sets the value of an option for a Config.
type Option interface {
apply(config) config
}
// WithWriter sets the export stream destination.
// func WithWriter(w io.Writer) Option {
// return writerOption{w}
// }
// type writerOption struct {
// W *gelf.Writer
// }
// func (o writerOption) apply(cfg config) config {
// cfg.Writer = o.W
// return cfg
// }
func WithGelfUrl(url string) Option {
return gelfUrlOption(url)
}
type gelfUrlOption string
func (o gelfUrlOption) apply(cfg config) config {
cfg.GelfUrl = string(o)
return cfg
}
// // WithPrettyPrint prettifies the emitted output.
// func WithPrettyPrint() Option {
// return prettyPrintOption(true)
// }
// type prettyPrintOption bool
// func (o prettyPrintOption) apply(cfg config) config {
// cfg.PrettyPrint = bool(o)
// return cfg
// }
// // WithoutTimestamps sets the export stream to not include timestamps.
// func WithoutTimestamps() Option {
// return timestampsOption(false)
// }
// type timestampsOption bool
// func (o timestampsOption) apply(cfg config) config {
// cfg.Timestamps = bool(o)
// return cfg
// }

46
pkg/gelf_exporter/gelf.go Normal file
View File

@ -0,0 +1,46 @@
package gelfexporter
import (
"log"
"maal/tracer/pkg/level"
"time"
"gopkg.in/Graylog2/go-gelf.v2/gelf"
)
type GELFMessage struct {
// Name of the application
Host string `json:"host"`
// Short, descriptive message
ShortMessage string `json:"short_message"`
// Optional long message.
LongMessage string `json:"long_message,omitempty"`
// Timestamp in Unix
Timestamp time.Time `json:"timestamp"`
// Severity level matching Syslog standard.
Level level.SyslogLevel `json:"level"`
// All additional field names must start with an underline.
ExtraFields map[string]interface{} `json:"extrafields,omitempty"`
}
func Log(writer *gelf.UDPWriter, msg GELFMessage) {
if writer != nil {
err := writer.WriteMessage(msg.GELFFormat())
if err != nil {
log.Println(err)
}
}
}
func (g GELFMessage) GELFFormat() *gelf.Message {
return &gelf.Message{
Version: "1.1",
Host: g.Host,
Short: g.ShortMessage,
Full: g.LongMessage,
TimeUnix: float64(g.Timestamp.Unix()),
Level: int32(g.Level),
Extra: g.ExtraFields,
}
}

104
pkg/gelf_exporter/trace.go Normal file
View File

@ -0,0 +1,104 @@
package gelfexporter
import (
"context"
"fmt"
"sync"
"time"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"gopkg.in/Graylog2/go-gelf.v2/gelf"
)
var zeroTime time.Time
var _ trace.SpanExporter = &Exporter{}
// New creates an Exporter with the passed options.
func New(options ...Option) (*Exporter, error) {
cfg, err := newConfig(options...)
if err != nil {
return nil, err
}
writer, err := gelf.NewUDPWriter(cfg.GelfUrl)
if err != nil {
return nil, err
}
return &Exporter{
gelfWriter: writer,
}, nil
}
// Exporter is an implementation of trace.SpanSyncer that writes spans to stdout.
type Exporter struct {
timestamps bool
gelfWriter *gelf.UDPWriter
stoppedMu sync.RWMutex
stopped bool
}
// ExportSpans writes spans in json format to stdout.
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
if err := ctx.Err(); err != nil {
return err
}
e.stoppedMu.RLock()
stopped := e.stopped
e.stoppedMu.RUnlock()
if stopped {
return nil
}
if len(spans) == 0 {
return nil
}
stubs := tracetest.SpanStubsFromReadOnlySpans(spans)
for i := range stubs {
stub := &stubs[i]
// Remove timestamps
if !e.timestamps {
stub.StartTime = zeroTime
stub.EndTime = zeroTime
for j := range stub.Events {
ev := &stub.Events[j]
ev.Time = zeroTime
}
}
fmt.Printf("stub: %v\n", stub)
Log(e.gelfWriter, GELFMessage{
Host: "test",
ShortMessage: "test",
})
}
return nil
}
// Shutdown is called to stop the exporter, it performs no action.
func (e *Exporter) Shutdown(ctx context.Context) error {
e.stoppedMu.Lock()
e.stopped = true
e.stoppedMu.Unlock()
return nil
}
// // MarshalLog is the marshaling function used by the logging system to represent this Exporter.
// func (e *Exporter) MarshalLog() interface{} {
// return struct {
// Type string
// WithTimestamps bool
// }{
// Type: "stdout",
// WithTimestamps: e.timestamps,
// }
// }

84
pkg/level/level.go Normal file
View File

@ -0,0 +1,84 @@
package level
import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type SyslogLevel uint8
const (
EMERG SyslogLevel = iota
ALERT
CRIT
ERR
WARNING
NOTICE
INFO
DEBUG
)
// Level Keyword Description
// 0 emergencies System is unusable
// 1 alerts Immediate action is needed
// 2 critical Critical conditions exist
// 3 errors Error conditions exist
// 4 warnings Warning conditions exist
// 5 notification Normal, but significant, conditions exist
// 6 informational Informational messages
// 7 debugging Debugging messages
func (l SyslogLevel) String() string {
switch l {
case EMERG:
return "EMERG"
case ALERT:
return "ALERT"
case CRIT:
return "CRIT"
case ERR:
return "ERR"
case WARNING:
return "WARN"
case NOTICE:
return "NOTICE"
case INFO:
return "INFO"
case DEBUG:
return "DEBUG"
default:
return "CRIT"
}
}
func LevelFromString(level string) SyslogLevel {
switch level {
case "EMERG":
return EMERG
case "ALERT":
return ALERT
case "CRIT":
return CRIT
case "ERR":
return ERR
case "WARN":
return WARNING
case "NOTICE":
return NOTICE
case "INFO":
return INFO
case "DEBUG":
return DEBUG
default:
return CRIT
}
}
func (lvl SyslogLevel) SetAttribute(att ...attribute.KeyValue) trace.SpanStartEventOption {
att = append(att, attribute.Int("level", int(lvl)))
return trace.WithAttributes(
att...,
)
}

160
pkg/tracer/middleware.go Normal file
View File

@ -0,0 +1,160 @@
package tracer
import (
"context"
"log"
gelfexporter "maal/tracer/pkg/gelf_exporter"
"os"
"github.com/gofiber/fiber/v2"
fiberOpentelemetry "github.com/psmarcin/fiber-opentelemetry/pkg/fiber-otel"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
trc "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
trace "go.opentelemetry.io/otel/trace"
)
var (
TracingError error = nil
TP trc.TracerProvider
)
type CustomExporter struct {
jaeger *otlptrace.Exporter
stdouttrace *stdouttrace.Exporter
}
type Config struct {
AppName string
JaegerUrl string
GelfUrl string
Version string
}
func NewCustomExporter(jaegerUrl string) (trc.SpanExporter, error) {
var jaeg *otlptrace.Exporter
var outrace *stdouttrace.Exporter
var err error
outrace, err = stdouttrace.New(
stdouttrace.WithWriter(os.Stdout),
stdouttrace.WithPrettyPrint(),
stdouttrace.WithoutTimestamps(),
)
if err != nil {
return &CustomExporter{}, err
}
if len(jaegerUrl) > 0 {
jaeg = otlptracehttp.NewUnstarted(otlptracehttp.WithEndpointURL(jaegerUrl))
}
return &CustomExporter{
jaeger: jaeg,
stdouttrace: outrace,
}, nil
}
func (e *CustomExporter) ExportSpans(ctx context.Context, spans []trc.ReadOnlySpan) error {
if TracingError == nil {
if e.jaeger != nil {
err := e.jaeger.ExportSpans(ctx, spans)
return err
} else {
return e.printOnlyOnError(ctx, spans)
}
}
return nil
}
func (e *CustomExporter) printOnlyOnError(ctx context.Context, spans []trc.ReadOnlySpan) error {
var err error
for _, s := range spans {
if s.Status().Code == codes.Error {
err = e.stdouttrace.ExportSpans(ctx, spans)
break
}
}
return err
}
func (e *CustomExporter) Shutdown(ctx context.Context) error {
if e.jaeger != nil {
e.stdouttrace.Shutdown(ctx)
return e.jaeger.Shutdown(ctx)
} else {
return e.stdouttrace.Shutdown(ctx)
}
}
func newResource(config Config) *resource.Resource {
r := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(config.AppName),
semconv.ServiceVersionKey.String(config.Version),
attribute.String("service.provider", "maal"),
)
return r
}
func NewTracer(config Config) func(*fiber.Ctx) error {
l := log.New(os.Stdout, "", 0)
var tracerProviders []trc.TracerProviderOption
otlpExporter := otlptracehttp.NewUnstarted(otlptracehttp.WithEndpointURL(config.JaegerUrl))
gelfExporter, err := gelfexporter.New(gelfexporter.WithGelfUrl(config.GelfUrl))
if err != nil {
l.Fatal(err)
}
tracerProviders = append(tracerProviders, trc.WithBatcher(otlpExporter))
tracerProviders = append(tracerProviders, trc.WithBatcher(gelfExporter))
tracerProviders = append(tracerProviders, trc.WithResource(newResource(config)))
TP = *trc.NewTracerProvider(tracerProviders...)
otel.SetTracerProvider(&TP)
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
if err != TracingError {
TracingError = err
log.Println(err)
}
}))
tracer := TP.Tracer("fiber-otel-router")
return fiberOpentelemetry.New(
fiberOpentelemetry.Config{
Tracer: tracer,
SpanName: "{{ .Method }} {{ .Path }}",
TracerStartAttributes: []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindServer),
trace.WithNewRoot(),
},
},
)
}
func ShutdownTracer() {
if err := TP.Shutdown(context.Background()); err != nil {
log.Fatal(err)
}
}
func Handler(fc *fiber.Ctx) (context.Context, trace.Span) {
simpleCtx, span := fiberOpentelemetry.Tracer.Start(fc.UserContext(), fc.OriginalURL())
fc.SetUserContext(simpleCtx)
span.SetAttributes(attribute.String("service.layer", "handler"))
return simpleCtx, span
}