feat(otel): add more tracing when communicating with internal tools / external APIs

This commit is contained in:
Julien Neuhart
2026-03-27 16:50:24 +01:00
parent 4e9f63004d
commit c72be765b0
8 changed files with 898 additions and 112 deletions

View File

@@ -21,7 +21,10 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/mholt/archives" "github.com/mholt/archives"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
@@ -232,10 +235,18 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
return fmt.Errorf("filter URL: %w", err) return fmt.Errorf("filter URL: %w", err)
} }
logger.DebugContext(ctx, fmt.Sprintf("download file from '%s'", dl.Url)) dlCtx, dlSpan := gotenberg.Tracer().Start(ctx, "GET Download From",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress(dl.Url)),
)
logger.DebugContext(dlCtx, fmt.Sprintf("download file from '%s'", dl.Url))
req, err := retryablehttp.NewRequest(http.MethodGet, dl.Url, nil) req, err := retryablehttp.NewRequest(http.MethodGet, dl.Url, nil)
if err != nil { if err != nil {
dlSpan.RecordError(err)
dlSpan.SetStatus(codes.Error, err.Error())
dlSpan.End()
return fmt.Errorf("create request to '%s': %w", dl.Url, err) return fmt.Errorf("create request to '%s': %w", dl.Url, err)
} }
@@ -245,7 +256,7 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
} }
// Inject OTEL trace context into outbound request. // Inject OTEL trace context into outbound request.
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) otel.GetTextMapPropagator().Inject(dlCtx, propagation.HeaderCarrier(req.Header))
// Propagate correlation ID header. // Propagate correlation ID header.
if correlationIdHeader, ok := echoCtx.Get("correlationIdHeader").(string); ok { if correlationIdHeader, ok := echoCtx.Get("correlationIdHeader").(string); ok {
@@ -268,6 +279,9 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
dlSpan.RecordError(err)
dlSpan.SetStatus(codes.Error, err.Error())
dlSpan.End()
return WrapError( return WrapError(
fmt.Errorf("download file from to '%s': %w", dl.Url, err), fmt.Errorf("download file from to '%s': %w", dl.Url, err),
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Unable to download file from '%s': %s", dl.Url, err)), NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Unable to download file from '%s': %s", dl.Url, err)),
@@ -281,16 +295,24 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
}() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
dlErr := fmt.Errorf("download file from to '%s': got status: '%s'", dl.Url, resp.Status)
dlSpan.RecordError(dlErr)
dlSpan.SetStatus(codes.Error, dlErr.Error())
dlSpan.End()
return WrapError( return WrapError(
fmt.Errorf("download file from to '%s': got status: '%s'", dl.Url, resp.Status), dlErr,
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Unable to download file from '%s': got status: '%s'", dl.Url, resp.Status)), NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Unable to download file from '%s': got status: '%s'", dl.Url, resp.Status)),
) )
} }
contentDisposition := resp.Header.Get("Content-Disposition") contentDisposition := resp.Header.Get("Content-Disposition")
if contentDisposition == "" { if contentDisposition == "" {
dlErr := fmt.Errorf("no 'Content-Disposition' header from '%s'", dl.Url)
dlSpan.RecordError(dlErr)
dlSpan.SetStatus(codes.Error, dlErr.Error())
dlSpan.End()
return WrapError( return WrapError(
fmt.Errorf("no 'Content-Disposition' header from '%s'", dl.Url), dlErr,
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("No 'Content-Disposition' header from '%s'", dl.Url)), NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("No 'Content-Disposition' header from '%s'", dl.Url)),
) )
} }
@@ -300,16 +322,24 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
// See: https://github.com/golang/go/issues/69551. // See: https://github.com/golang/go/issues/69551.
_, params, err := mime.ParseMediaType(contentDisposition) _, params, err := mime.ParseMediaType(contentDisposition)
if err != nil { if err != nil {
dlErr := fmt.Errorf("parse 'Content-Disposition' header '%s' from '%s': %w", contentDisposition, dl.Url, err)
dlSpan.RecordError(dlErr)
dlSpan.SetStatus(codes.Error, dlErr.Error())
dlSpan.End()
return WrapError( return WrapError(
fmt.Errorf("parse 'Content-Disposition' header '%s' from '%s': %w", contentDisposition, dl.Url, err), dlErr,
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid 'Content-Disposition' header '%s' from '%s': %s", contentDisposition, dl.Url, err)), NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid 'Content-Disposition' header '%s' from '%s': %s", contentDisposition, dl.Url, err)),
) )
} }
filename, ok := params["filename"] filename, ok := params["filename"]
if !ok { if !ok {
dlErr := fmt.Errorf("get filename from 'Content-Disposition' header '%s' from '%s'", contentDisposition, dl.Url)
dlSpan.RecordError(dlErr)
dlSpan.SetStatus(codes.Error, dlErr.Error())
dlSpan.End()
return WrapError( return WrapError(
fmt.Errorf("get filename from 'Content-Disposition' header '%s' from '%s'", contentDisposition, dl.Url), dlErr,
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid 'Content-Disposition' header '%s' from '%s': no filename", contentDisposition, dl.Url)), NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid 'Content-Disposition' header '%s' from '%s': no filename", contentDisposition, dl.Url)),
) )
} }
@@ -322,7 +352,11 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
out, err := os.Create(path) out, err := os.Create(path)
if err != nil { if err != nil {
return fmt.Errorf("create local file: %w", err) dlErr := fmt.Errorf("create local file: %w", err)
dlSpan.RecordError(dlErr)
dlSpan.SetStatus(codes.Error, dlErr.Error())
dlSpan.End()
return dlErr
} }
defer func() { defer func() {
err := out.Close() err := out.Close()
@@ -336,9 +370,16 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
_, err = io.Copy(out, reader) _, err = io.Copy(out, reader)
if err != nil { if err != nil {
return fmt.Errorf("copy downloaded file from '%s' to local file: %w", dl.Url, err) dlErr := fmt.Errorf("copy downloaded file from '%s' to local file: %w", dl.Url, err)
dlSpan.RecordError(dlErr)
dlSpan.SetStatus(codes.Error, dlErr.Error())
dlSpan.End()
return dlErr
} }
dlSpan.SetStatus(codes.Ok, "")
dlSpan.End()
ctx.files[filename] = path ctx.files[filename] = path
// Route the downloaded file to the appropriate field bucket. // Route the downloaded file to the appropriate field bucket.

View File

@@ -16,7 +16,10 @@ import (
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api" "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
@@ -784,6 +787,12 @@ func (mod *Chromium) Routes() ([]api.Route, error) {
// Pdf converts a URL to PDF. // Pdf converts a URL to PDF.
func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPath string, options PdfOptions) error { func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPath string, options PdfOptions) error {
ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Pdf",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("chromium")),
)
defer span.End()
start := time.Now() start := time.Now()
var conversionStart time.Time var conversionStart time.Time
@@ -844,12 +853,23 @@ func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPa
if fileInfo, statErr := os.Stat(outputPath); statErr == nil { if fileInfo, statErr := os.Stat(outputPath); statErr == nil {
mod.pdfOutputSizeCounter.Record(ctx, fileInfo.Size()) mod.pdfOutputSizeCounter.Record(ctx, fileInfo.Size())
} }
span.SetStatus(codes.Ok, "")
return nil
} }
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err return err
} }
func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, outputPath string, options ScreenshotOptions) error { func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, outputPath string, options ScreenshotOptions) error {
ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Screenshot",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("chromium")),
)
defer span.End()
start := time.Now() start := time.Now()
var conversionStart time.Time var conversionStart time.Time
@@ -912,8 +932,13 @@ func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, o
if fileInfo, statErr := os.Stat(outputPath); statErr == nil { if fileInfo, statErr := os.Stat(outputPath); statErr == nil {
mod.imageOutputSizeCounter.Record(ctx, fileInfo.Size()) mod.imageOutputSizeCounter.Record(ctx, fileInfo.Size())
} }
span.SetStatus(codes.Ok, "")
return nil
} }
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err return err
} }

View File

@@ -12,6 +12,9 @@ import (
"syscall" "syscall"
"github.com/barasher/go-exiftool" "github.com/barasher/go-exiftool"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
@@ -75,29 +78,74 @@ func (engine *ExifTool) Debug() map[string]any {
// Merge is not available in this implementation. // Merge is not available in this implementation.
func (engine *ExifTool) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error { func (engine *ExifTool) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
return fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Merge",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Split is not available in this implementation. // Split is not available in this implementation.
func (engine *ExifTool) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { func (engine *ExifTool) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
return nil, fmt.Errorf("split PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Split",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("split PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// Flatten is not available in this implementation. // Flatten is not available in this implementation.
func (engine *ExifTool) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error { func (engine *ExifTool) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
return fmt.Errorf("flatten PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Flatten",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("flatten PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Convert is not available in this implementation. // Convert is not available in this implementation.
func (engine *ExifTool) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { func (engine *ExifTool) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Convert",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadMetadata extracts the metadata of a given PDF file. // ReadMetadata extracts the metadata of a given PDF file.
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) { func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
_, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath)) exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
if err != nil { if err != nil {
return nil, fmt.Errorf("new ExifTool: %w", err) err = fmt.Errorf("new ExifTool: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
defer func(exifTool *exiftool.Exiftool) { defer func(exifTool *exiftool.Exiftool) {
@@ -109,17 +157,30 @@ func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *slog.Logger, i
fileMetadata := exifTool.ExtractMetadata(inputPath) fileMetadata := exifTool.ExtractMetadata(inputPath)
if fileMetadata[0].Err != nil { if fileMetadata[0].Err != nil {
return nil, fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err) err = fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
span.SetStatus(codes.Ok, "")
return fileMetadata[0].Fields, nil return fileMetadata[0].Fields, nil
} }
// WriteMetadata writes the metadata into a given PDF file. // WriteMetadata writes the metadata into a given PDF file.
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error { func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
_, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath)) exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
if err != nil { if err != nil {
return fmt.Errorf("new ExifTool: %w", err) err = fmt.Errorf("new ExifTool: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
defer func(exifTool *exiftool.Exiftool) { defer func(exifTool *exiftool.Exiftool) {
@@ -131,7 +192,10 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger,
fileMetadata := exifTool.ExtractMetadata(inputPath) fileMetadata := exifTool.ExtractMetadata(inputPath)
if fileMetadata[0].Err != nil { if fileMetadata[0].Err != nil {
return fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err) err = fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Define a list of derived, system, or computed tags that ExifTool // Define a list of derived, system, or computed tags that ExifTool
@@ -175,7 +239,10 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger,
strs[i] = str strs[i] = str
continue continue
} }
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeFor[[]any](), gotenberg.ErrPdfEngineMetadataValueNotSupported) err = fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeFor[[]any](), gotenberg.ErrPdfEngineMetadataValueNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
fileMetadata[0].SetStrings(key, strs) fileMetadata[0].SetStrings(key, strs)
case bool: case bool:
@@ -191,82 +258,174 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger,
// TODO: support more complex cases, e.g., arrays and nested objects // TODO: support more complex cases, e.g., arrays and nested objects
// (limitations in underlying library). // (limitations in underlying library).
default: default:
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported) err = fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
} }
exifTool.WriteMetadata(fileMetadata) exifTool.WriteMetadata(fileMetadata)
if fileMetadata[0].Err != nil { if fileMetadata[0].Err != nil {
return fmt.Errorf("write PDF metadata with ExifTool: %w", fileMetadata[0].Err) err = fmt.Errorf("write PDF metadata with ExifTool: %w", fileMetadata[0].Err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// PageCount returns the number of pages in a PDF file using ExifTool. // PageCount returns the number of pages in a PDF file using ExifTool.
func (engine *ExifTool) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) { func (engine *ExifTool) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
_, span := gotenberg.Tracer().Start(ctx, "exiftool.PageCount",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
metadata, err := engine.ReadMetadata(ctx, logger, inputPath) metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
if err != nil { if err != nil {
return 0, fmt.Errorf("read metadata with ExifTool: %w", err) err = fmt.Errorf("read metadata with ExifTool: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
pageCountValue, ok := metadata["PageCount"] pageCountValue, ok := metadata["PageCount"]
if !ok { if !ok {
return 0, errors.New("PageCount not found in metadata") err = errors.New("PageCount not found in metadata")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
switch val := pageCountValue.(type) { switch val := pageCountValue.(type) {
case int: case int:
span.SetStatus(codes.Ok, "")
return val, nil return val, nil
case int64: case int64:
span.SetStatus(codes.Ok, "")
return int(val), nil return int(val), nil
case float64: case float64:
span.SetStatus(codes.Ok, "")
return int(val), nil return int(val), nil
case string: case string:
var res int var res int
_, err := fmt.Sscanf(val, "%d", &res) _, err := fmt.Sscanf(val, "%d", &res)
if err != nil { if err != nil {
return 0, fmt.Errorf("parse PageCount string '%s': %w", val, err) err = fmt.Errorf("parse PageCount string '%s': %w", val, err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
span.SetStatus(codes.Ok, "")
return res, nil return res, nil
default: default:
return 0, fmt.Errorf("unexpected PageCount type '%T'", pageCountValue) err = fmt.Errorf("unexpected PageCount type '%T'", pageCountValue)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
} }
// WriteBookmarks is not available in this implementation. // WriteBookmarks is not available in this implementation.
func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error { func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("write PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadBookmarks is not available in this implementation. // ReadBookmarks is not available in this implementation.
func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) { func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
return nil, fmt.Errorf("read PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("read PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// Encrypt is not available in this implementation. // Encrypt is not available in this implementation.
func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error { func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
return fmt.Errorf("encrypt PDF using ExifTool: %w", gotenberg.ErrPdfEncryptionNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Encrypt",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("encrypt PDF using ExifTool: %w", gotenberg.ErrPdfEncryptionNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// EmbedFiles is not available in this implementation. // EmbedFiles is not available in this implementation.
func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error { func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.EmbedFiles",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("embed files with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Watermark is not available in this implementation. // Watermark is not available in this implementation.
func (engine *ExifTool) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *ExifTool) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("watermark PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Watermark",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("watermark PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Stamp is not available in this implementation. // Stamp is not available in this implementation.
func (engine *ExifTool) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *ExifTool) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("stamp PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Stamp",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("stamp PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Rotate is not available in this implementation. // Rotate is not available in this implementation.
func (engine *ExifTool) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error { func (engine *ExifTool) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
return fmt.Errorf("rotate PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "exiftool.Rotate",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("exiftool")),
)
defer span.End()
err := fmt.Errorf("rotate PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Interface guards. // Interface guards.

View File

@@ -14,7 +14,10 @@ import (
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -528,6 +531,12 @@ func (a *Api) LibreOffice() (Uno, error) {
// Pdf converts a document to PDF. // Pdf converts a document to PDF.
func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error { func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error {
ctx, span := gotenberg.Tracer().Start(ctx, "libreoffice.Pdf",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("libreoffice")),
)
defer span.End()
start := time.Now() start := time.Now()
var conversionStart time.Time var conversionStart time.Time
@@ -579,6 +588,7 @@ func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPat
a.pdfOutputSizeCounter.Record(ctx, stat.Size(), attrs) a.pdfOutputSizeCounter.Record(ctx, stat.Size(), attrs)
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
@@ -588,6 +598,8 @@ func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPat
return a.Pdf(ctx, logger, inputPath, outputPath, options) return a.Pdf(ctx, logger, inputPath, outputPath, options)
} }
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return fmt.Errorf("supervisor run task: %w", err) return fmt.Errorf("supervisor run task: %w", err)
} }

View File

@@ -15,6 +15,10 @@ import (
"strings" "strings"
"syscall" "syscall"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
@@ -96,25 +100,44 @@ func (engine *PdfCpu) Debug() map[string]any {
// Merge combines multiple PDFs into a single PDF. // Merge combines multiple PDFs into a single PDF.
func (engine *PdfCpu) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error { func (engine *PdfCpu) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Merge",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
args := make([]string, 0, 2+len(inputPaths)) args := make([]string, 0, 2+len(inputPaths))
args = append(args, "merge", outputPath) args = append(args, "merge", outputPath)
args = append(args, inputPaths...) args = append(args, inputPaths...)
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err == nil { if err == nil {
span.SetStatus(codes.Ok, "")
return nil return nil
} }
return fmt.Errorf("merge PDFs with pdfcpu: %w", err) err = fmt.Errorf("merge PDFs with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Split splits a given PDF file. // Split splits a given PDF file.
func (engine *PdfCpu) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { func (engine *PdfCpu) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Split",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
var args []string var args []string
switch mode.Mode { switch mode.Mode {
@@ -128,17 +151,26 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *slog.Logger, mode goten
} }
args = append(args, "extract", "-mode", "page", "-pages", mode.Span, inputPath, outputDirPath) args = append(args, "extract", "-mode", "page", "-pages", mode.Span, inputPath, outputDirPath)
default: default:
return nil, fmt.Errorf("split PDFs using mode '%s' with pdfcpu: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) err := fmt.Errorf("split PDFs using mode '%s' with pdfcpu: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return nil, fmt.Errorf("split PDFs with pdfcpu: %w", err) err = fmt.Errorf("split PDFs with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
var outputPaths []string var outputPaths []string
@@ -155,46 +187,104 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *slog.Logger, mode goten
return nil return nil
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("walk directory to find resulting PDFs from split with pdfcpu: %w", err) err = fmt.Errorf("walk directory to find resulting PDFs from split with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
sort.Sort(digitSuffixSort(outputPaths)) sort.Sort(digitSuffixSort(outputPaths))
span.SetStatus(codes.Ok, "")
return outputPaths, nil return outputPaths, nil
} }
// Flatten is not available in this implementation. // Flatten is not available in this implementation.
func (engine *PdfCpu) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error { func (engine *PdfCpu) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
return fmt.Errorf("flatten PDF with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Flatten",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := fmt.Errorf("flatten PDF with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Convert is not available in this implementation. // Convert is not available in this implementation.
func (engine *PdfCpu) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { func (engine *PdfCpu) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with pdfcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Convert",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := fmt.Errorf("convert PDF to '%+v' with pdfcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) { func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
return nil, fmt.Errorf("read PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := fmt.Errorf("read PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error { func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// PageCount is not available in this implementation. // PageCount is not available in this implementation.
func (engine *PdfCpu) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) { func (engine *PdfCpu) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdfcpu.PageCount",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := fmt.Errorf("page count with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
// ReadBookmarks reads the document outline (bookmarks) of a PDF file using pdfcpu. // ReadBookmarks reads the document outline (bookmarks) of a PDF file using pdfcpu.
func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) { func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
tmpPath := fmt.Sprintf("%s.read.json", inputPath) tmpPath := fmt.Sprintf("%s.read.json", inputPath)
args := []string{"bookmarks", "export", inputPath, tmpPath} args := []string{"bookmarks", "export", inputPath, tmpPath}
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
defer func() { defer func() {
@@ -213,41 +303,55 @@ func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, in
// If the file wasn't created, or it was created but is 0 bytes, // If the file wasn't created, or it was created but is 0 bytes,
// it means pdfcpu had no bookmarks to write. // it means pdfcpu had no bookmarks to write.
if os.IsNotExist(statErr) || (statErr == nil && info.Size() == 0) { if os.IsNotExist(statErr) || (statErr == nil && info.Size() == 0) {
span.SetStatus(codes.Ok, "")
return make([]gotenberg.Bookmark, 0), nil return make([]gotenberg.Bookmark, 0), nil
} }
// Fallback: Check the error string just in case pdfcpu failed without // Fallback: Check the error string just in case pdfcpu failed without
// touching the file. // touching the file.
if strings.Contains(strings.ToLower(cmdErr.Error()), "no bookmarks") { if strings.Contains(strings.ToLower(cmdErr.Error()), "no bookmarks") {
span.SetStatus(codes.Ok, "")
return make([]gotenberg.Bookmark, 0), nil return make([]gotenberg.Bookmark, 0), nil
} }
return nil, fmt.Errorf("read bookmarks with pdfcpu: %w", cmdErr) err = fmt.Errorf("read bookmarks with pdfcpu: %w", cmdErr)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// If cmd succeeded, but output a 0-byte file anyway. // If cmd succeeded, but output a 0-byte file anyway.
if info != nil && info.Size() == 0 { if info != nil && info.Size() == 0 {
span.SetStatus(codes.Ok, "")
return make([]gotenberg.Bookmark, 0), nil return make([]gotenberg.Bookmark, 0), nil
} }
// Read the file content. // Read the file content.
jsonBytes, err := os.ReadFile(tmpPath) jsonBytes, err := os.ReadFile(tmpPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("read temporary bookmarks JSON file: %w", err) err = fmt.Errorf("read temporary bookmarks JSON file: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// Check if the content is just empty whitespace. // Check if the content is just empty whitespace.
if len(bytes.TrimSpace(jsonBytes)) == 0 { if len(bytes.TrimSpace(jsonBytes)) == 0 {
span.SetStatus(codes.Ok, "")
return make([]gotenberg.Bookmark, 0), nil return make([]gotenberg.Bookmark, 0), nil
} }
var data pdfcpuBookmarks var data pdfcpuBookmarks
err = json.Unmarshal(jsonBytes, &data) err = json.Unmarshal(jsonBytes, &data)
if err != nil { if err != nil {
return nil, fmt.Errorf("unmarshal bookmarks: %w", err) err = fmt.Errorf("unmarshal bookmarks: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// Safety check: Does the parsed JSON actually contain bookmarks? // Safety check: Does the parsed JSON actually contain bookmarks?
if len(data.Bookmarks) == 0 { if len(data.Bookmarks) == 0 {
span.SetStatus(codes.Ok, "")
return make([]gotenberg.Bookmark, 0), nil return make([]gotenberg.Bookmark, 0), nil
} }
@@ -264,12 +368,20 @@ func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, in
return res return res
} }
span.SetStatus(codes.Ok, "")
return mapBookmarks(data.Bookmarks), nil return mapBookmarks(data.Bookmarks), nil
} }
// WriteBookmarks adds a document outline (bookmarks) to a PDF file using pdfcpu. // WriteBookmarks adds a document outline (bookmarks) to a PDF file using pdfcpu.
func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error { func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
if len(bookmarks) == 0 { if len(bookmarks) == 0 {
span.SetStatus(codes.Ok, "")
return nil return nil
} }
@@ -292,13 +404,19 @@ func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, i
jsonBytes, err := json.Marshal(data) jsonBytes, err := json.Marshal(data)
if err != nil { if err != nil {
return fmt.Errorf("marshal bookmarks: %w", err) err = fmt.Errorf("marshal bookmarks: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
tmpPath := fmt.Sprintf("%s.json", inputPath) tmpPath := fmt.Sprintf("%s.json", inputPath)
err = os.WriteFile(tmpPath, jsonBytes, 0o600) err = os.WriteFile(tmpPath, jsonBytes, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("write temporary bookmarks JSON file: %w", err) err = fmt.Errorf("write temporary bookmarks JSON file: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
defer func() { defer func() {
@@ -311,21 +429,35 @@ func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, i
args := []string{"bookmarks", "import", "-replace", inputPath, tmpPath, inputPath} args := []string{"bookmarks", "import", "-replace", inputPath, tmpPath, inputPath}
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("write bookmarks with pdfcpu: %w", err) err = fmt.Errorf("write bookmarks with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// EmbedFiles embeds files into a PDF. All files are embedded as file attachments // EmbedFiles embeds files into a PDF. All files are embedded as file attachments
// without modifying the main PDF content. // without modifying the main PDF content.
func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error { func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.EmbedFiles",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
if len(filePaths) == 0 { if len(filePaths) == 0 {
span.SetStatus(codes.Ok, "")
return nil return nil
} }
@@ -337,21 +469,37 @@ func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *slog.Logger, fileP
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command for attaching files: %w", err) err = fmt.Errorf("create command for attaching files: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("attach files with pdfcpu: %w", err) err = fmt.Errorf("attach files with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// Encrypt adds password protection to a PDF file using pdfcpu. // Encrypt adds password protection to a PDF file using pdfcpu.
func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error { func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Encrypt",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
if userPassword == "" { if userPassword == "" {
return errors.New("user password cannot be empty") err := errors.New("user password cannot be empty")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
if ownerPassword == "" { if ownerPassword == "" {
@@ -368,29 +516,70 @@ func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPat
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("encrypt PDF with pdfcpu: %w", err) err = fmt.Errorf("encrypt PDF with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// Watermark applies a watermark (behind page content) to a PDF file using pdfcpu. // Watermark applies a watermark (behind page content) to a PDF file using pdfcpu.
func (engine *PdfCpu) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *PdfCpu) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
return engine.applyStampOrWatermark(ctx, logger, "watermark", inputPath, stamp) ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Watermark",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := engine.applyStampOrWatermark(ctx, logger, "watermark", inputPath, stamp)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
} }
// Stamp applies a stamp (on top of page content) to a PDF file using pdfcpu. // Stamp applies a stamp (on top of page content) to a PDF file using pdfcpu.
func (engine *PdfCpu) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *PdfCpu) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
return engine.applyStampOrWatermark(ctx, logger, "stamp", inputPath, stamp) ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Stamp",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
err := engine.applyStampOrWatermark(ctx, logger, "stamp", inputPath, stamp)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
} }
// Rotate rotates pages of a PDF file by the given angle using pdfcpu. // Rotate rotates pages of a PDF file by the given angle using pdfcpu.
func (engine *PdfCpu) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error { func (engine *PdfCpu) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Rotate",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdfcpu")),
)
defer span.End()
args := []string{"rotate"} args := []string{"rotate"}
if pages != "" { if pages != "" {
args = append(args, "-pages", pages) args = append(args, "-pages", pages)
@@ -399,14 +588,21 @@ func (engine *PdfCpu) Rotate(ctx context.Context, logger *slog.Logger, inputPath
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("rotate PDF with pdfcpu: %w", err) err = fmt.Errorf("rotate PDF with pdfcpu: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }

View File

@@ -11,6 +11,10 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
@@ -79,94 +83,201 @@ func (engine *PdfTk) Debug() map[string]any {
// Split splits a given PDF file. // Split splits a given PDF file.
func (engine *PdfTk) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { func (engine *PdfTk) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Split",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
var args []string var args []string
outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath)) outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath))
switch mode.Mode { switch mode.Mode {
case gotenberg.SplitModePages: case gotenberg.SplitModePages:
if !mode.Unify { if !mode.Unify {
return nil, fmt.Errorf("split PDFs using mode '%s' without unify with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) err := fmt.Errorf("split PDFs using mode '%s' without unify with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
args = append(args, inputPath, "cat", mode.Span, "output", outputPath) args = append(args, inputPath, "cat", mode.Span, "output", outputPath)
default: default:
return nil, fmt.Errorf("split PDFs using mode '%s' with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) err := fmt.Errorf("split PDFs using mode '%s' with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return nil, fmt.Errorf("split PDFs with PDFtk: %w", err) err = fmt.Errorf("split PDFs with PDFtk: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
span.SetStatus(codes.Ok, "")
return []string{outputPath}, nil return []string{outputPath}, nil
} }
// Merge combines multiple PDFs into a single PDF. // Merge combines multiple PDFs into a single PDF.
func (engine *PdfTk) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error { func (engine *PdfTk) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Merge",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
args := make([]string, 0, 3+len(inputPaths)) args := make([]string, 0, 3+len(inputPaths))
args = append(args, inputPaths...) args = append(args, inputPaths...)
args = append(args, "cat", "output", outputPath) args = append(args, "cat", "output", outputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err == nil { if err == nil {
span.SetStatus(codes.Ok, "")
return nil return nil
} }
return fmt.Errorf("merge PDFs with PDFtk: %w", err) err = fmt.Errorf("merge PDFs with PDFtk: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Flatten is not available in this implementation. // Flatten is not available in this implementation.
func (engine *PdfTk) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error { func (engine *PdfTk) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
return fmt.Errorf("flatten PDF with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.Flatten",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("flatten PDF with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Convert is not available in this implementation. // Convert is not available in this implementation.
func (engine *PdfTk) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { func (engine *PdfTk) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.Convert",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) { func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
return nil, fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error { func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// PageCount is not available in this implementation. // PageCount is not available in this implementation.
func (engine *PdfTk) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) { func (engine *PdfTk) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.PageCount",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("page count with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
// WriteBookmarks is not available in this implementation. // WriteBookmarks is not available in this implementation.
func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error { func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("write PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadBookmarks is not available in this implementation. // ReadBookmarks is not available in this implementation.
func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) { func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
return nil, fmt.Errorf("read PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("read PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// Encrypt adds password protection to a PDF file using PDFtk. // Encrypt adds password protection to a PDF file using PDFtk.
func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error { func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Encrypt",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
if userPassword == "" { if userPassword == "" {
return errors.New("user password cannot be empty") err := errors.New("user password cannot be empty")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
if ownerPassword == userPassword || ownerPassword == "" { if ownerPassword == userPassword || ownerPassword == "" {
return gotenberg.NewPdfEngineInvalidArgs("pdftk", "both 'userPassword' and 'ownerPassword' must be provided and different. Consider switching to another PDF engine if this behavior does not work with your workflow") err := gotenberg.NewPdfEngineInvalidArgs("pdftk", "both 'userPassword' and 'ownerPassword' must be provided and different. Consider switching to another PDF engine if this behavior does not work with your workflow")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Create a temp output file in the same directory. // Create a temp output file in the same directory.
@@ -181,32 +292,62 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("encrypt PDF with PDFtk: %w", err) err = fmt.Errorf("encrypt PDF with PDFtk: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
err = os.Rename(tmpPath, inputPath) err = os.Rename(tmpPath, inputPath)
if err != nil { if err != nil {
return fmt.Errorf("rename temporary output file with input file: %w", err) err = fmt.Errorf("rename temporary output file with input file: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// EmbedFiles is not available in this implementation. // EmbedFiles is not available in this implementation.
func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error { func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "pdftk.EmbedFiles",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
err := fmt.Errorf("embed files with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Watermark applies a watermark (behind page content) to a PDF file using PDFtk. // Watermark applies a watermark (behind page content) to a PDF file using PDFtk.
// Only PDF source is supported. // Only PDF source is supported.
//
//nolint:dupl
func (engine *PdfTk) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *PdfTk) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Watermark",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
if stamp.Source != gotenberg.StampSourcePDF { if stamp.Source != gotenberg.StampSourcePDF {
return fmt.Errorf("watermark PDF with PDFtk: %w", gotenberg.ErrPdfStampSourceNotSupported) err := fmt.Errorf("watermark PDF with PDFtk: %w", gotenberg.ErrPdfStampSourceNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
tmpPath := inputPath + ".tmp" tmpPath := inputPath + ".tmp"
@@ -215,27 +356,48 @@ func (engine *PdfTk) Watermark(ctx context.Context, logger *slog.Logger, inputPa
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("watermark PDF with PDFtk: %w", err) err = fmt.Errorf("watermark PDF with PDFtk: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
err = os.Rename(tmpPath, inputPath) err = os.Rename(tmpPath, inputPath)
if err != nil { if err != nil {
return fmt.Errorf("rename temporary output file with input file: %w", err) err = fmt.Errorf("rename temporary output file with input file: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// Stamp applies a stamp (on top of page content) to a PDF file using PDFtk. // Stamp applies a stamp (on top of page content) to a PDF file using PDFtk.
// Only PDF source is supported. // Only PDF source is supported.
//
//nolint:dupl
func (engine *PdfTk) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *PdfTk) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Stamp",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
if stamp.Source != gotenberg.StampSourcePDF { if stamp.Source != gotenberg.StampSourcePDF {
return fmt.Errorf("stamp PDF with PDFtk: %w", gotenberg.ErrPdfStampSourceNotSupported) err := fmt.Errorf("stamp PDF with PDFtk: %w", gotenberg.ErrPdfStampSourceNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
tmpPath := inputPath + ".tmp" tmpPath := inputPath + ".tmp"
@@ -244,19 +406,29 @@ func (engine *PdfTk) Stamp(ctx context.Context, logger *slog.Logger, inputPath s
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("stamp PDF with PDFtk: %w", err) err = fmt.Errorf("stamp PDF with PDFtk: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
err = os.Rename(tmpPath, inputPath) err = os.Rename(tmpPath, inputPath)
if err != nil { if err != nil {
return fmt.Errorf("rename temporary output file with input file: %w", err) err = fmt.Errorf("rename temporary output file with input file: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
@@ -264,8 +436,17 @@ func (engine *PdfTk) Stamp(ctx context.Context, logger *slog.Logger, inputPath s
// Page-specific rotation is not supported; if pages is non-empty, // Page-specific rotation is not supported; if pages is non-empty,
// ErrPdfEngineMethodNotSupported is returned. // ErrPdfEngineMethodNotSupported is returned.
func (engine *PdfTk) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error { func (engine *PdfTk) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Rotate",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("pdftk")),
)
defer span.End()
if pages != "" { if pages != "" {
return fmt.Errorf("rotate PDF with PDFtk (page-specific rotation): %w", gotenberg.ErrPdfEngineMethodNotSupported) err := fmt.Errorf("rotate PDF with PDFtk (page-specific rotation): %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
var direction string var direction string
@@ -277,7 +458,10 @@ func (engine *PdfTk) Rotate(ctx context.Context, logger *slog.Logger, inputPath
case 270: case 270:
direction = "west" direction = "west"
default: default:
return fmt.Errorf("rotate PDF with PDFtk: %w", gotenberg.ErrPdfRotateAngleNotSupported) err := fmt.Errorf("rotate PDF with PDFtk: %w", gotenberg.ErrPdfRotateAngleNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
tmpPath := inputPath + ".tmp" tmpPath := inputPath + ".tmp"
@@ -285,19 +469,29 @@ func (engine *PdfTk) Rotate(ctx context.Context, logger *slog.Logger, inputPath
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("rotate PDF with PDFtk: %w", err) err = fmt.Errorf("rotate PDF with PDFtk: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
err = os.Rename(tmpPath, inputPath) err = os.Rename(tmpPath, inputPath)
if err != nil { if err != nil {
return fmt.Errorf("rename temporary output file with input file: %w", err) err = fmt.Errorf("rename temporary output file with input file: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }

View File

@@ -11,6 +11,10 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
@@ -82,37 +86,62 @@ func (engine *QPdf) Debug() map[string]any {
// Split splits a given PDF file. // Split splits a given PDF file.
func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Split",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
var args []string var args []string
outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath)) outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath))
switch mode.Mode { switch mode.Mode {
case gotenberg.SplitModePages: case gotenberg.SplitModePages:
if !mode.Unify { if !mode.Unify {
return nil, fmt.Errorf("split PDFs using mode '%s' without unify with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) err := fmt.Errorf("split PDFs using mode '%s' without unify with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
args = append(args, inputPath) args = append(args, inputPath)
args = append(args, engine.globalArgs...) args = append(args, engine.globalArgs...)
args = append(args, "--pages", ".", mode.Span) args = append(args, "--pages", ".", mode.Span)
args = append(args, "--", outputPath) args = append(args, "--", outputPath)
default: default:
return nil, fmt.Errorf("split PDFs using mode '%s' with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) err := fmt.Errorf("split PDFs using mode '%s' with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return nil, fmt.Errorf("split PDFs with QPDF: %w", err) err = fmt.Errorf("split PDFs with QPDF: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
span.SetStatus(codes.Ok, "")
return []string{outputPath}, nil return []string{outputPath}, nil
} }
// Merge combines multiple PDFs into a single PDF. // Merge combines multiple PDFs into a single PDF.
func (engine *QPdf) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error { func (engine *QPdf) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Merge",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
args := make([]string, 0, 4+len(engine.globalArgs)+len(inputPaths)) args := make([]string, 0, 4+len(engine.globalArgs)+len(inputPaths))
args = append(args, "--empty") args = append(args, "--empty")
args = append(args, engine.globalArgs...) args = append(args, engine.globalArgs...)
@@ -122,20 +151,33 @@ func (engine *QPdf) Merge(ctx context.Context, logger *slog.Logger, inputPaths [
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err == nil { if err == nil {
span.SetStatus(codes.Ok, "")
return nil return nil
} }
return fmt.Errorf("merge PDFs with QPDF: %w", err) err = fmt.Errorf("merge PDFs with QPDF: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Flatten merges annotation appearances with page content, deleting the // Flatten merges annotation appearances with page content, deleting the
// original annotations. // original annotations.
func (engine *QPdf) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error { func (engine *QPdf) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Flatten",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
args := make([]string, 0, 4+len(engine.globalArgs)) args := make([]string, 0, 4+len(engine.globalArgs))
args = append(args, inputPath) args = append(args, inputPath)
args = append(args, "--generate-appearances") args = append(args, "--generate-appearances")
@@ -145,51 +187,121 @@ func (engine *QPdf) Flatten(ctx context.Context, logger *slog.Logger, inputPath
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err == nil { if err == nil {
span.SetStatus(codes.Ok, "")
return nil return nil
} }
return fmt.Errorf("flatten PDFs with QPDF: %w", err) err = fmt.Errorf("flatten PDFs with QPDF: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Convert is not available in this implementation. // Convert is not available in this implementation.
func (engine *QPdf) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { func (engine *QPdf) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.Convert",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) { func (engine *QPdf) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
return nil, fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error { func (engine *QPdf) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteMetadata",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// PageCount is not available in this implementation. // PageCount is not available in this implementation.
func (engine *QPdf) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) { func (engine *QPdf) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.PageCount",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("page count with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
} }
// WriteBookmarks is not available in this implementation. // WriteBookmarks is not available in this implementation.
func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error { func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("write PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// ReadBookmarks is not available in this implementation. // ReadBookmarks is not available in this implementation.
func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) { func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
return nil, fmt.Errorf("read PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadBookmarks",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("read PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
} }
// Encrypt adds password protection to a PDF file using QPDF. // Encrypt adds password protection to a PDF file using QPDF.
func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error { func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Encrypt",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
if userPassword == "" { if userPassword == "" {
return errors.New("user password cannot be empty") err := errors.New("user password cannot be empty")
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
if ownerPassword == "" { if ownerPassword == "" {
@@ -204,35 +316,78 @@ func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath,
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil { if err != nil {
return fmt.Errorf("create command: %w", err) err = fmt.Errorf("create command: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
_, err = cmd.Exec() _, err = cmd.Exec()
if err != nil { if err != nil {
return fmt.Errorf("encrypt PDF with QPDF: %w", err) err = fmt.Errorf("encrypt PDF with QPDF: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
span.SetStatus(codes.Ok, "")
return nil return nil
} }
// EmbedFiles is not available in this implementation. // EmbedFiles is not available in this implementation.
func (engine *QPdf) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error { func (engine *QPdf) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFiles",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("embed files with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Watermark is not available in this implementation. // Watermark is not available in this implementation.
func (engine *QPdf) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *QPdf) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("watermark PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.Watermark",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("watermark PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Stamp is not available in this implementation. // Stamp is not available in this implementation.
func (engine *QPdf) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error { func (engine *QPdf) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("stamp PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.Stamp",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("stamp PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
// Rotate is not available in this implementation. // Rotate is not available in this implementation.
func (engine *QPdf) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error { func (engine *QPdf) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
return fmt.Errorf("rotate PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) _, span := gotenberg.Tracer().Start(ctx, "qpdf.Rotate",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress("qpdf")),
)
defer span.End()
err := fmt.Errorf("rotate PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
} }
var ( var (

View File

@@ -14,6 +14,7 @@ import (
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -50,7 +51,10 @@ func (c client) send(ctx context.Context, body io.Reader, headers map[string]str
} }
tracer := gotenberg.Tracer() tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient)) ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconv.ServerAddress(url)),
)
defer span.End() defer span.End()
req, err := retryablehttp.NewRequest(method, url, body) req, err := retryablehttp.NewRequest(method, url, body)