feat(otel): add OpenTelemetry support

This commit is contained in:
Julien Neuhart
2026-03-27 16:28:45 +01:00
parent 08088c15f4
commit 4e9f63004d
86 changed files with 4396 additions and 1283 deletions

View File

@@ -3,6 +3,7 @@ package chromium
import (
"context"
"fmt"
"log/slog"
"net/http"
"net/url"
"slices"
@@ -17,7 +18,6 @@ import (
"github.com/chromedp/chromedp"
"github.com/dlclark/regexp2"
"go.uber.org/multierr"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -33,29 +33,28 @@ type eventRequestPausedOptions struct {
// allowed or not. It also set the extra HTTP headers, if any.
// See https://github.com/gotenberg/gotenberg/issues/1011.
// TODO: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setBlockedURLs (experimental for now).
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, options eventRequestPausedOptions) {
func listenForEventRequestPaused(ctx context.Context, logger *slog.Logger, options eventRequestPausedOptions) {
if len(options.extraHttpHeaders) == 0 {
logger.Debug("no extra HTTP headers")
logger.DebugContext(ctx, "no extra HTTP headers")
} else {
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", options.extraHttpHeaders))
logger.DebugContext(ctx, fmt.Sprintf("extra HTTP headers: %+v", options.extraHttpHeaders))
}
chromedp.ListenTarget(ctx, func(ev any) {
switch e := ev.(type) {
case *fetch.EventRequestPaused:
if e, ok := ev.(*fetch.EventRequestPaused); ok {
go func() {
logger.Debug(fmt.Sprintf("event EventRequestPaused fired for '%s'", e.Request.URL))
logger.DebugContext(ctx, fmt.Sprintf("event EventRequestPaused fired for '%s'", e.Request.URL))
allow := true
deadline, ok := ctx.Deadline()
if !ok {
logger.Error("context has no deadline, cannot filter URL")
logger.ErrorContext(ctx, "context has no deadline, cannot filter URL")
return
}
err := gotenberg.FilterDeadline(options.allowList, options.denyList, e.Request.URL, deadline)
if err != nil {
logger.Warn(err.Error())
logger.WarnContext(ctx, err.Error())
allow = false
}
@@ -73,7 +72,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
}
if !prefixMatch {
logger.Warn(fmt.Sprintf("'%s' is not within any allowed file prefix", e.Request.URL))
logger.WarnContext(ctx, fmt.Sprintf("'%s' is not within any allowed file prefix", e.Request.URL))
allow = false
}
}
@@ -85,7 +84,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
err = req.Do(executorCtx)
if err != nil {
logger.Error(fmt.Sprintf("fail request: %s", err))
logger.ErrorContext(ctx, fmt.Sprintf("fail request: %s", err))
}
return
}
@@ -101,25 +100,26 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
for _, header := range options.extraHttpHeaders {
if header.Scope == nil {
// Non-scoped header.
logger.Debug(fmt.Sprintf("extra HTTP header '%s' will be set for request URL '%s'", header.Name, e.Request.URL))
logger.DebugContext(ctx, fmt.Sprintf("extra HTTP header '%s' will be set for request URL '%s'", header.Name, e.Request.URL))
extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
continue
}
ok, err := header.Scope.MatchString(e.Request.URL)
if err != nil {
logger.Error(fmt.Sprintf("fail to match extra HTTP header '%s' scope with URL '%s': %s", header.Name, e.Request.URL, err))
} else if ok {
logger.Debug(fmt.Sprintf("extra HTTP header '%s' (scoped) will be set for request URL '%s'", header.Name, e.Request.URL))
switch {
case err != nil:
logger.ErrorContext(ctx, fmt.Sprintf("fail to match extra HTTP header '%s' scope with URL '%s': %s", header.Name, e.Request.URL, err))
case ok:
logger.DebugContext(ctx, fmt.Sprintf("extra HTTP header '%s' (scoped) will be set for request URL '%s'", header.Name, e.Request.URL))
extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
} else {
logger.Debug(fmt.Sprintf("scoped extra HTTP header '%s' (scoped) will not be set for request URL '%s'", header.Name, e.Request.URL))
default:
logger.DebugContext(ctx, fmt.Sprintf("scoped extra HTTP header '%s' (scoped) will not be set for request URL '%s'", header.Name, e.Request.URL))
}
}
}
if len(extraHttpHeadersToSet) > 0 {
logger.Debug(fmt.Sprintf("setting extra HTTP headers for request URL '%s': %+v", e.Request.URL, extraHttpHeadersToSet))
logger.DebugContext(ctx, fmt.Sprintf("setting extra HTTP headers for request URL '%s': %+v", e.Request.URL, extraHttpHeadersToSet))
originalHeaders := e.Request.Headers
headers := make(map[string]string)
@@ -129,7 +129,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
if ok {
headers[key] = strValue
} else {
logger.Error(fmt.Sprintf("ignoring header '%s' for URL '%s' since it cannot be cast to a string", key, e.Request.URL))
logger.ErrorContext(ctx, fmt.Sprintf("ignoring header '%s' for URL '%s' since it cannot be cast to a string", key, e.Request.URL))
}
}
@@ -152,7 +152,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
err = req.Do(executorCtx)
if err != nil {
logger.Error(fmt.Sprintf("continue request: %s", err))
logger.ErrorContext(ctx, fmt.Sprintf("continue request: %s", err))
}
}()
}
@@ -177,7 +177,7 @@ type eventResponseReceivedOptions struct {
// https://github.com/gotenberg/gotenberg/issues/1021.
func listenForEventResponseReceived(
ctx context.Context,
logger *zap.Logger,
logger *slog.Logger,
options eventResponseReceivedOptions,
) {
normalizedIgnoreDomains := normalizeDomains(options.ignoreResourceHttpStatusDomains)
@@ -197,10 +197,9 @@ func listenForEventResponseReceived(
}
chromedp.ListenTarget(ctx, func(ev any) {
switch ev := ev.(type) {
case *network.EventResponseReceived:
if ev, ok := ev.(*network.EventResponseReceived); ok {
if ev.Response.URL == options.mainPageUrl {
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response))
logger.DebugContext(ctx, fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response))
if slices.Contains(options.failOnHttpStatusCodes, ev.Response.Status) {
options.invalidHttpStatusCodeMu.Lock()
@@ -212,11 +211,11 @@ func listenForEventResponseReceived(
return
}
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response))
logger.DebugContext(ctx, fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response))
if slices.Contains(options.failOnResourceOnHttpStatusCode, ev.Response.Status) {
if !shouldCheckResourceHttpStatusCode(ev.Response.URL, normalizedIgnoreDomains) {
logger.Debug(fmt.Sprintf("skip resource HTTP status code check for '%s' due to domain filtering", ev.Response.URL))
logger.DebugContext(ctx, fmt.Sprintf("skip resource HTTP status code check for '%s' due to domain filtering", ev.Response.URL))
return
}
@@ -318,11 +317,10 @@ type eventLoadingFailedOptions struct {
// https://github.com/gotenberg/gotenberg/issues/913.
// https://github.com/gotenberg/gotenberg/issues/959.
// https://github.com/gotenberg/gotenberg/issues/1021.
func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, options eventLoadingFailedOptions) {
func listenForEventLoadingFailed(ctx context.Context, logger *slog.Logger, options eventLoadingFailedOptions) {
chromedp.ListenTarget(ctx, func(ev any) {
switch ev := ev.(type) {
case *network.EventLoadingFailed:
logger.Debug(fmt.Sprintf("event EventLoadingFailed fired: %+v", ev.ErrorText))
if ev, ok := ev.(*network.EventLoadingFailed); ok {
logger.DebugContext(ctx, fmt.Sprintf("event EventLoadingFailed fired: %+v", ev.ErrorText))
// We are looking for common errors.
// TODO: sufficient?
@@ -341,14 +339,14 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, option
"net::ERR_HTTP2_PROTOCOL_ERROR",
}
if !slices.Contains(errors, ev.ErrorText) {
logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors))
logger.DebugContext(ctx, fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors))
return
}
if ev.Type == network.ResourceTypeDocument {
// Supposition: except iframe, an event loading failed with a
// resource type Document is about the main page.
logger.Debug("event EventLoadingFailed fired for main page")
logger.DebugContext(ctx, "event EventLoadingFailed fired for main page")
options.loadingFailedMu.Lock()
defer options.loadingFailedMu.Unlock()
@@ -358,7 +356,7 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, option
return
}
logger.Debug("event EventLoadingFailed fired for a resource")
logger.DebugContext(ctx, "event EventLoadingFailed fired for a resource")
options.resourceLoadingFailedMu.Lock()
defer options.resourceLoadingFailedMu.Unlock()
@@ -374,11 +372,10 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, option
// listenForEventExceptionThrown listens for exceptions in the console and
// appends those exceptions to the given error pointer.
// See https://github.com/gotenberg/gotenberg/issues/262.
func listenForEventExceptionThrown(ctx context.Context, logger *zap.Logger, consoleExceptions *error, consoleExceptionsMu *sync.RWMutex) {
func listenForEventExceptionThrown(ctx context.Context, logger *slog.Logger, consoleExceptions *error, consoleExceptionsMu *sync.RWMutex) {
chromedp.ListenTarget(ctx, func(ev any) {
switch ev := ev.(type) {
case *runtime.EventExceptionThrown:
logger.Debug(fmt.Sprintf("event EventExceptionThrown fired: %+v", ev.ExceptionDetails))
if ev, ok := ev.(*runtime.EventExceptionThrown); ok {
logger.DebugContext(ctx, fmt.Sprintf("event EventExceptionThrown fired: %+v", ev.ExceptionDetails))
consoleExceptionsMu.Lock()
defer consoleExceptionsMu.Unlock()
@@ -390,13 +387,12 @@ func listenForEventExceptionThrown(ctx context.Context, logger *zap.Logger, cons
// waitForEventDomContentEventFired waits until the event DomContentEventFired
// is fired or the context timeout.
func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) func() error {
func waitForEventDomContentEventFired(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
switch ev.(type) {
case *page.EventDomContentEventFired:
if _, ok := ev.(*page.EventDomContentEventFired); ok {
cancel()
close(ch)
}
@@ -404,7 +400,7 @@ func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) f
select {
case <-ch:
logger.Debug("event DomContentEventFired fired")
logger.DebugContext(ctx, "event DomContentEventFired fired")
return nil
case <-ctx.Done():
return fmt.Errorf("wait for event DomContentEventFired: %w", ctx.Err())
@@ -414,13 +410,12 @@ func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) f
// waitForEventLoadEventFired waits until the event LoadEventFired is fired or
// the context timeout.
func waitForEventLoadEventFired(ctx context.Context, logger *zap.Logger) func() error {
func waitForEventLoadEventFired(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
switch ev.(type) {
case *page.EventLoadEventFired:
if _, ok := ev.(*page.EventLoadEventFired); ok {
cancel()
close(ch)
}
@@ -428,7 +423,7 @@ func waitForEventLoadEventFired(ctx context.Context, logger *zap.Logger) func()
select {
case <-ch:
logger.Debug("event LoadEventFired fired")
logger.DebugContext(ctx, "event LoadEventFired fired")
return nil
case <-ctx.Done():
return fmt.Errorf("wait for event LoadEventFired: %w", ctx.Err())
@@ -438,23 +433,20 @@ func waitForEventLoadEventFired(ctx context.Context, logger *zap.Logger) func()
// waitForEventNetworkIdle waits until the event networkIdle is fired or the
// context timeout.
func waitForEventNetworkIdle(ctx context.Context, logger *zap.Logger) func() error {
func waitForEventNetworkIdle(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
switch e := ev.(type) {
case *page.EventLifecycleEvent:
if e.Name == "networkIdle" {
cancel()
close(ch)
}
if e, ok := ev.(*page.EventLifecycleEvent); ok && e.Name == "networkIdle" {
cancel()
close(ch)
}
})
select {
case <-ch:
logger.Debug("event networkIdle fired")
logger.DebugContext(ctx, "event networkIdle fired")
return nil
case <-ctx.Done():
return fmt.Errorf("wait for event networkIdle: %w", ctx.Err())
@@ -469,12 +461,9 @@ func waitForEventNetworkAlmostIdle(ctx context.Context, logger *slog.Logger) fun
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
switch e := ev.(type) {
case *page.EventLifecycleEvent:
if e.Name == "networkIdle2" {
cancel()
close(ch)
}
if e, ok := ev.(*page.EventLifecycleEvent); ok && e.Name == "networkIdle2" {
cancel()
close(ch)
}
})
@@ -490,13 +479,12 @@ func waitForEventNetworkAlmostIdle(ctx context.Context, logger *slog.Logger) fun
// waitForEventLoadingFinished waits until the event LoadingFinished is fired
// or the context timeout.
func waitForEventLoadingFinished(ctx context.Context, logger *zap.Logger) func() error {
func waitForEventLoadingFinished(ctx context.Context, logger *slog.Logger) func() error {
return func() error {
ch := make(chan struct{})
cctx, cancel := context.WithCancel(ctx)
chromedp.ListenTarget(cctx, func(ev any) {
switch ev.(type) {
case *network.EventLoadingFinished:
if _, ok := ev.(*network.EventLoadingFinished); ok {
cancel()
close(ch)
}
@@ -504,7 +492,7 @@ func waitForEventLoadingFinished(ctx context.Context, logger *zap.Logger) func()
select {
case <-ch:
logger.Debug("event LoadingFinished fired")
logger.DebugContext(ctx, "event LoadingFinished fired")
return nil
case <-ctx.Done():
return fmt.Errorf("wait for event LoadingFinished: %w", ctx.Err())