diff --git a/pkg/modules/api/context.go b/pkg/modules/api/context.go index 4c925e5f..43d298e6 100644 --- a/pkg/modules/api/context.go +++ b/pkg/modules/api/context.go @@ -21,7 +21,10 @@ import ( "github.com/labstack/echo/v4" "github.com/mholt/archives" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" "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/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) } - 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) 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) } @@ -245,7 +256,7 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys } // 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. 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) if err != nil { + dlSpan.RecordError(err) + dlSpan.SetStatus(codes.Error, err.Error()) + dlSpan.End() return WrapError( 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)), @@ -281,16 +295,24 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys }() 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( - 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)), ) } contentDisposition := resp.Header.Get("Content-Disposition") 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( - fmt.Errorf("no 'Content-Disposition' header from '%s'", dl.Url), + dlErr, 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. _, params, err := mime.ParseMediaType(contentDisposition) 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( - 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)), ) } filename, ok := params["filename"] 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( - 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)), ) } @@ -322,7 +352,11 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys out, err := os.Create(path) 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() { err := out.Close() @@ -336,9 +370,16 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys _, err = io.Copy(out, reader) 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 // Route the downloaded file to the appropriate field bucket. diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index fe22a3d1..04f62c72 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -16,7 +16,10 @@ import ( "github.com/dlclark/regexp2" flag "github.com/spf13/pflag" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "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/modules/api" @@ -784,6 +787,12 @@ func (mod *Chromium) Routes() ([]api.Route, error) { // Pdf converts a URL to PDF. 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() 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 { mod.pdfOutputSizeCounter.Record(ctx, fileInfo.Size()) } + + span.SetStatus(codes.Ok, "") + return nil } + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return err } 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() 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 { mod.imageOutputSizeCounter.Record(ctx, fileInfo.Size()) } + + span.SetStatus(codes.Ok, "") + return nil } + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return err } diff --git a/pkg/modules/exiftool/exiftool.go b/pkg/modules/exiftool/exiftool.go index db91c8cc..52b629a5 100644 --- a/pkg/modules/exiftool/exiftool.go +++ b/pkg/modules/exiftool/exiftool.go @@ -12,6 +12,9 @@ import ( "syscall" "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" ) @@ -75,29 +78,74 @@ func (engine *ExifTool) Debug() map[string]any { // Merge is not available in this implementation. 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. 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. 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. 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. 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)) 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) { @@ -109,17 +157,30 @@ func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *slog.Logger, i fileMetadata := exifTool.ExtractMetadata(inputPath) 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 } // 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 { + _, 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)) 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) { @@ -131,7 +192,10 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger, fileMetadata := exifTool.ExtractMetadata(inputPath) 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 @@ -175,7 +239,10 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger, strs[i] = str 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) 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 // (limitations in underlying library). 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) 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 } // 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) { + _, 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) 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"] 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) { case int: + span.SetStatus(codes.Ok, "") return val, nil case int64: + span.SetStatus(codes.Ok, "") return int(val), nil case float64: + span.SetStatus(codes.Ok, "") return int(val), nil case string: var res int _, err := fmt.Sscanf(val, "%d", &res) 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 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. 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. 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. 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. 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. 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. 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. 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. diff --git a/pkg/modules/libreoffice/api/api.go b/pkg/modules/libreoffice/api/api.go index 00b56438..396395f0 100644 --- a/pkg/modules/libreoffice/api/api.go +++ b/pkg/modules/libreoffice/api/api.go @@ -14,7 +14,10 @@ import ( "github.com/alexliesenfeld/health" flag "github.com/spf13/pflag" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" + "go.opentelemetry.io/otel/trace" "go.uber.org/multierr" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" @@ -528,6 +531,12 @@ func (a *Api) LibreOffice() (Uno, error) { // Pdf converts a document to PDF. 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() 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) } + span.SetStatus(codes.Ok, "") 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) } + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return fmt.Errorf("supervisor run task: %w", err) } diff --git a/pkg/modules/pdfcpu/pdfcpu.go b/pkg/modules/pdfcpu/pdfcpu.go index 461bd9d3..c77d3286 100644 --- a/pkg/modules/pdfcpu/pdfcpu.go +++ b/pkg/modules/pdfcpu/pdfcpu.go @@ -15,6 +15,10 @@ import ( "strings" "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" ) @@ -96,25 +100,44 @@ func (engine *PdfCpu) Debug() map[string]any { // Merge combines multiple PDFs into a single PDF. 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 = append(args, "merge", outputPath) args = append(args, inputPaths...) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() if err == nil { + span.SetStatus(codes.Ok, "") 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. 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 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) 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...) 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() 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 @@ -155,46 +187,104 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *slog.Logger, mode goten return 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)) + span.SetStatus(codes.Ok, "") return outputPaths, nil } // Flatten is not available in this implementation. 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. 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. 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. 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. 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. 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) args := []string{"bookmarks", "export", inputPath, tmpPath} cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() { @@ -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, // it means pdfcpu had no bookmarks to write. if os.IsNotExist(statErr) || (statErr == nil && info.Size() == 0) { + span.SetStatus(codes.Ok, "") return make([]gotenberg.Bookmark, 0), nil } // Fallback: Check the error string just in case pdfcpu failed without // touching the file. if strings.Contains(strings.ToLower(cmdErr.Error()), "no bookmarks") { + span.SetStatus(codes.Ok, "") 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 info != nil && info.Size() == 0 { + span.SetStatus(codes.Ok, "") return make([]gotenberg.Bookmark, 0), nil } // Read the file content. jsonBytes, err := os.ReadFile(tmpPath) 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. if len(bytes.TrimSpace(jsonBytes)) == 0 { + span.SetStatus(codes.Ok, "") return make([]gotenberg.Bookmark, 0), nil } var data pdfcpuBookmarks err = json.Unmarshal(jsonBytes, &data) 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? if len(data.Bookmarks) == 0 { + span.SetStatus(codes.Ok, "") return make([]gotenberg.Bookmark, 0), nil } @@ -264,12 +368,20 @@ func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, in return res } + span.SetStatus(codes.Ok, "") return mapBookmarks(data.Bookmarks), nil } // 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 { + ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteBookmarks", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes(semconv.ServerAddress("pdfcpu")), + ) + defer span.End() + if len(bookmarks) == 0 { + span.SetStatus(codes.Ok, "") return nil } @@ -292,13 +404,19 @@ func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, i jsonBytes, err := json.Marshal(data) 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) err = os.WriteFile(tmpPath, jsonBytes, 0o600) 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() { @@ -311,21 +429,35 @@ func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, i args := []string{"bookmarks", "import", "-replace", inputPath, tmpPath, inputPath} cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() 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 } // EmbedFiles embeds files into a PDF. All files are embedded as file attachments // without modifying the main PDF content. 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 { + span.SetStatus(codes.Ok, "") 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...) 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() 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 } // 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 { + ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Encrypt", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes(semconv.ServerAddress("pdfcpu")), + ) + defer span.End() + 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 == "" { @@ -368,29 +516,70 @@ func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPat cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() 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 } // 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 { - 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. 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. 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"} if 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...) 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() 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 } diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index 43bfc8ab..0eb58859 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -11,6 +11,10 @@ import ( "path/filepath" "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" ) @@ -79,94 +83,201 @@ func (engine *PdfTk) Debug() map[string]any { // Split splits a given PDF file. 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 outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath)) switch mode.Mode { case gotenberg.SplitModePages: 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) 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...) 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() 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 } // Merge combines multiple PDFs into a single PDF. 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 = append(args, inputPaths...) args = append(args, "cat", "output", outputPath) cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() if err == nil { + span.SetStatus(codes.Ok, "") 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. 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. 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. 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. 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. 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. 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. 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. 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 == "" { - 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 == "" { - 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. @@ -181,32 +292,62 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() 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) 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 } // EmbedFiles is not available in this implementation. 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. // Only PDF source is supported. +// +//nolint:dupl 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 { - 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" @@ -215,27 +356,48 @@ func (engine *PdfTk) Watermark(ctx context.Context, logger *slog.Logger, inputPa cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() 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) 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 } // Stamp applies a stamp (on top of page content) to a PDF file using PDFtk. // Only PDF source is supported. +// +//nolint:dupl 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 { - 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" @@ -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...) 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() 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) 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 } @@ -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, // ErrPdfEngineMethodNotSupported is returned. 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 != "" { - 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 @@ -277,7 +458,10 @@ func (engine *PdfTk) Rotate(ctx context.Context, logger *slog.Logger, inputPath case 270: direction = "west" 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" @@ -285,19 +469,29 @@ func (engine *PdfTk) Rotate(ctx context.Context, logger *slog.Logger, inputPath cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() 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) 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 } diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index 34d5c2a5..7c4b2cb9 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -11,6 +11,10 @@ import ( "path/filepath" "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" ) @@ -82,37 +86,62 @@ func (engine *QPdf) Debug() map[string]any { // Split splits a given PDF file. 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 outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath)) switch mode.Mode { case gotenberg.SplitModePages: 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, engine.globalArgs...) args = append(args, "--pages", ".", mode.Span) args = append(args, "--", outputPath) 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...) 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() 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 } // Merge combines multiple PDFs into a single PDF. 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 = append(args, "--empty") 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...) 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() if err == nil { + span.SetStatus(codes.Ok, "") 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 // original annotations. 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 = append(args, inputPath) 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...) 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() if err == nil { + span.SetStatus(codes.Ok, "") 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. 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. 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. 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. 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. 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. 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. 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 == "" { - 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 == "" { @@ -204,35 +316,78 @@ func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) 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() 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 } // EmbedFiles is not available in this implementation. 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. 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. 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. 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 ( diff --git a/pkg/modules/webhook/client.go b/pkg/modules/webhook/client.go index 5a45da31..b3d6e115 100644 --- a/pkg/modules/webhook/client.go +++ b/pkg/modules/webhook/client.go @@ -14,6 +14,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" "go.opentelemetry.io/otel/trace" "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() - 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() req, err := retryablehttp.NewRequest(method, url, body)