diff --git a/pkg/modules/chromium/attrs_test.go b/pkg/modules/chromium/attrs_test.go
index 3ab9edb5..72ca405b 100644
--- a/pkg/modules/chromium/attrs_test.go
+++ b/pkg/modules/chromium/attrs_test.go
@@ -4,11 +4,43 @@ import (
"context"
"os"
"path/filepath"
+ "strings"
"testing"
+ "go.opentelemetry.io/otel/attribute"
+
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
)
+func TestPrintToPdfAttrs(t *testing.T) {
+ options := DefaultPdfOptions()
+ options.Landscape = true
+ options.PageRanges = "1-5"
+ options.HeaderTemplate = "
secret header
"
+ // FooterTemplate left at default, so has_footer must be false.
+
+ got := map[string]attribute.Value{}
+ for _, kv := range printToPdfAttrs(options) {
+ got[string(kv.Key)] = kv.Value
+ if s := kv.Value.AsString(); strings.Contains(s, "secret") || s == "1-5" {
+ t.Errorf("attribute %s leaked a raw value: %q", kv.Key, s)
+ }
+ }
+
+ if !got["gotenberg.chromium.print.landscape"].AsBool() {
+ t.Error("expected landscape=true")
+ }
+ if !got["gotenberg.chromium.print.has_page_ranges"].AsBool() {
+ t.Error("expected has_page_ranges=true")
+ }
+ if !got["gotenberg.chromium.print.has_header"].AsBool() {
+ t.Error("expected has_header=true")
+ }
+ if got["gotenberg.chromium.print.has_footer"].AsBool() {
+ t.Error("expected has_footer=false")
+ }
+}
+
func TestConversionInputAttrs(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "index.html")
content := []byte("")
diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go
index f1777a4a..2fc491c2 100644
--- a/pkg/modules/chromium/browser.go
+++ b/pkg/modules/chromium/browser.go
@@ -334,7 +334,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// PDF specific.
- printToPdfActionFunc(logger, outputPath, options),
+ printToPdfActionFunc(ctx, logger, outputPath, options),
// Teardown.
page.Close(),
})
diff --git a/pkg/modules/chromium/tasks.go b/pkg/modules/chromium/tasks.go
index 56e8b459..59f883bb 100644
--- a/pkg/modules/chromium/tasks.go
+++ b/pkg/modules/chromium/tasks.go
@@ -14,104 +14,148 @@ import (
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/codes"
+ "go.opentelemetry.io/otel/trace"
+
+ "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
-func printToPdfActionFunc(logger *slog.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
+func printToPdfActionFunc(reqCtx context.Context, logger *slog.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
return func(ctx context.Context) error {
- paperHeight := options.PaperHeight
- pageRanges := options.PageRanges
+ // ctx is the chromedp task context, derived from context.Background(),
+ // so the span is started under reqCtx to keep print_to_pdf in the
+ // conversion trace instead of orphaning it into a new one.
+ _, span := gotenberg.Tracer().Start(reqCtx, "chromium.print_to_pdf",
+ trace.WithSpanKind(trace.SpanKindClient),
+ trace.WithAttributes(printToPdfAttrs(options)...),
+ )
+ defer span.End()
- if options.SinglePage {
- logger.DebugContext(ctx, "single page PDF")
+ err := func() error {
+ paperHeight := options.PaperHeight
+ pageRanges := options.PageRanges
- _, _, _, _, _, cssContentSize, err := page.GetLayoutMetrics().Do(ctx)
- if err != nil {
- return fmt.Errorf("get layout metrics: %w", err)
+ if options.SinglePage {
+ logger.DebugContext(ctx, "single page PDF")
+
+ _, _, _, _, _, cssContentSize, err := page.GetLayoutMetrics().Do(ctx)
+ if err != nil {
+ return fmt.Errorf("get layout metrics: %w", err)
+ }
+
+ // There are 96 CSS pixels per inch.
+ // See https://issues.chromium.org/issues/40267771#comment14.
+ // We add top and bottom margins so that the content area
+ // is large enough to fit the entire content.
+ paperHeight = (cssContentSize.Height / 96) + options.MarginTop + options.MarginBottom
+ pageRanges = "1" // little dirty hack to avoid leftovers.
}
- // There are 96 CSS pixels per inch.
- // See https://issues.chromium.org/issues/40267771#comment14.
- // We add top and bottom margins so that the content area
- // is large enough to fit the entire content.
- paperHeight = (cssContentSize.Height / 96) + options.MarginTop + options.MarginBottom
- pageRanges = "1" // little dirty hack to avoid leftovers.
- }
+ printToPdf := page.PrintToPDF().
+ WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
+ WithLandscape(options.Landscape).
+ WithPrintBackground(options.PrintBackground).
+ WithScale(options.Scale).
+ WithPaperWidth(options.PaperWidth).
+ WithPaperHeight(paperHeight).
+ WithMarginTop(options.MarginTop).
+ WithMarginBottom(options.MarginBottom).
+ WithMarginLeft(options.MarginLeft).
+ WithMarginRight(options.MarginRight).
+ WithPageRanges(pageRanges).
+ WithPreferCSSPageSize(options.PreferCssPageSize).
+ WithGenerateDocumentOutline(options.GenerateDocumentOutline).
+ // See https://github.com/gotenberg/gotenberg/issues/1210.
+ WithGenerateTaggedPDF(options.GenerateTaggedPdf)
- printToPdf := page.PrintToPDF().
- WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
- WithLandscape(options.Landscape).
- WithPrintBackground(options.PrintBackground).
- WithScale(options.Scale).
- WithPaperWidth(options.PaperWidth).
- WithPaperHeight(paperHeight).
- WithMarginTop(options.MarginTop).
- WithMarginBottom(options.MarginBottom).
- WithMarginLeft(options.MarginLeft).
- WithMarginRight(options.MarginRight).
- WithPageRanges(pageRanges).
- WithPreferCSSPageSize(options.PreferCssPageSize).
- WithGenerateDocumentOutline(options.GenerateDocumentOutline).
- // See https://github.com/gotenberg/gotenberg/issues/1210.
- WithGenerateTaggedPDF(options.GenerateTaggedPdf)
+ hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
+ options.FooterTemplate != DefaultPdfOptions().FooterTemplate
- hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
- options.FooterTemplate != DefaultPdfOptions().FooterTemplate
+ if !hasCustomHeaderFooter {
+ logger.DebugContext(ctx, "no custom header nor footer")
- if !hasCustomHeaderFooter {
- logger.DebugContext(ctx, "no custom header nor footer")
+ printToPdf = printToPdf.WithDisplayHeaderFooter(false)
+ } else {
+ logger.DebugContext(ctx, "with custom header and/or footer")
- printToPdf = printToPdf.WithDisplayHeaderFooter(false)
+ printToPdf = printToPdf.
+ WithDisplayHeaderFooter(true).
+ WithHeaderTemplate(options.HeaderTemplate).
+ WithFooterTemplate(options.FooterTemplate)
+ }
+
+ logger.DebugContext(ctx, fmt.Sprintf("print to PDF with: %+v", printToPdf))
+
+ _, stream, err := printToPdf.Do(ctx)
+ if err != nil {
+ return fmt.Errorf("print to PDF: %w", err)
+ }
+
+ reader := &streamReader{
+ ctx: ctx,
+ handle: stream,
+ r: nil,
+ pos: 0,
+ eof: false,
+ }
+
+ defer func() {
+ err = reader.Close()
+ if err != nil {
+ logger.ErrorContext(ctx, fmt.Sprintf("close reader: %s", err))
+ }
+ }()
+
+ file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
+ if err != nil {
+ return fmt.Errorf("open output path: %w", err)
+ }
+
+ defer func() {
+ err = file.Close()
+ if err != nil {
+ logger.ErrorContext(ctx, fmt.Sprintf("close output path: %s", err))
+ }
+ }()
+
+ buffer := bufio.NewReader(reader)
+
+ _, err = buffer.WriteTo(file)
+ if err != nil {
+ return fmt.Errorf("write result to output path: %w", err)
+ }
+
+ return nil
+ }()
+
+ if err != nil {
+ span.RecordError(err)
+ span.SetStatus(codes.Error, err.Error())
} else {
- logger.DebugContext(ctx, "with custom header and/or footer")
-
- printToPdf = printToPdf.
- WithDisplayHeaderFooter(true).
- WithHeaderTemplate(options.HeaderTemplate).
- WithFooterTemplate(options.FooterTemplate)
+ span.SetStatus(codes.Ok, "")
}
- logger.DebugContext(ctx, fmt.Sprintf("print to PDF with: %+v", printToPdf))
+ return err
+ }
+}
- _, stream, err := printToPdf.Do(ctx)
- if err != nil {
- return fmt.Errorf("print to PDF: %w", err)
- }
-
- reader := &streamReader{
- ctx: ctx,
- handle: stream,
- r: nil,
- pos: 0,
- eof: false,
- }
-
- defer func() {
- err = reader.Close()
- if err != nil {
- logger.ErrorContext(ctx, fmt.Sprintf("close reader: %s", err))
- }
- }()
-
- file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
- if err != nil {
- return fmt.Errorf("open output path: %w", err)
- }
-
- defer func() {
- err = file.Close()
- if err != nil {
- logger.ErrorContext(ctx, fmt.Sprintf("close output path: %s", err))
- }
- }()
-
- buffer := bufio.NewReader(reader)
-
- _, err = buffer.WriteTo(file)
- if err != nil {
- return fmt.Errorf("write result to output path: %w", err)
- }
-
- return nil
+// printToPdfAttrs derives bounded, low-cardinality attributes from the print
+// options. Raw header/footer templates and page ranges are reduced to booleans
+// to avoid leaking document content and exploding cardinality.
+func printToPdfAttrs(options PdfOptions) []attribute.KeyValue {
+ return []attribute.KeyValue{
+ attribute.Bool("gotenberg.chromium.print.landscape", options.Landscape),
+ attribute.Bool("gotenberg.chromium.print.print_background", options.PrintBackground),
+ attribute.Float64("gotenberg.chromium.print.scale", options.Scale),
+ attribute.Float64("gotenberg.chromium.print.paper_width", options.PaperWidth),
+ attribute.Float64("gotenberg.chromium.print.paper_height", options.PaperHeight),
+ attribute.Bool("gotenberg.chromium.print.single_page", options.SinglePage),
+ attribute.Bool("gotenberg.chromium.print.prefer_css_page_size", options.PreferCssPageSize),
+ attribute.Bool("gotenberg.chromium.print.generate_tagged_pdf", options.GenerateTaggedPdf),
+ attribute.Bool("gotenberg.chromium.print.has_page_ranges", options.PageRanges != ""),
+ attribute.Bool("gotenberg.chromium.print.has_header", options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate),
+ attribute.Bool("gotenberg.chromium.print.has_footer", options.FooterTemplate != DefaultPdfOptions().FooterTemplate),
}
}