mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
feat(telemetry): record backing-binary versions on spans, captured at build time
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -101,6 +102,9 @@ type Chromium struct {
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
engine gotenberg.PdfEngine
|
||||
|
||||
version string
|
||||
versionOnce sync.Once
|
||||
|
||||
reqsCounter metric.Int64Counter
|
||||
errsCounter metric.Int64Counter
|
||||
conversionDurationCounter metric.Float64Histogram
|
||||
@@ -717,19 +721,46 @@ func (mod *Chromium) Stop(ctx context.Context) error {
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (mod *Chromium) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
return map[string]any{"version": mod.detectVersion()}
|
||||
}
|
||||
|
||||
cmd := exec.Command(mod.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
// detectVersion resolves the Chromium version once, preferring the value
|
||||
// captured at image build time so it never spawns Chromium at runtime. It falls
|
||||
// back to running chromium --version for local or non-Docker builds.
|
||||
func (mod *Chromium) detectVersion() string {
|
||||
mod.versionOnce.Do(func() {
|
||||
if v, ok := gotenberg.BuildVersion("chromium"); ok {
|
||||
mod.version = v
|
||||
return
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
cmd := exec.Command(mod.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
mod.version = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
mod.version = strings.TrimSpace(string(output))
|
||||
})
|
||||
|
||||
return mod.version
|
||||
}
|
||||
|
||||
// spanAttrs returns the client-span attributes for a Chromium invocation: the
|
||||
// server address and the Chromium version, plus any extra attributes. The
|
||||
// version rides on every conversion span so a trace records which Chromium
|
||||
// rendered the document.
|
||||
func (mod *Chromium) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
|
||||
attrs = append(attrs, semconv.ServerAddress(mod.args.binPath))
|
||||
if v := mod.detectVersion(); v != "" {
|
||||
attrs = append(attrs, attribute.String("gotenberg.chromium.version", v))
|
||||
}
|
||||
|
||||
debug["version"] = strings.TrimSpace(string(output))
|
||||
return debug
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// Metrics returns the metrics.
|
||||
@@ -828,7 +859,7 @@ func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPa
|
||||
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Pdf",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(mod.args.binPath)),
|
||||
trace.WithAttributes(mod.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -909,7 +940,7 @@ func (mod *Chromium) Pdf(ctx context.Context, logger *slog.Logger, url, outputPa
|
||||
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(mod.args.binPath)),
|
||||
trace.WithAttributes(mod.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
|
||||
37
pkg/modules/chromium/version_test.go
Normal file
37
pkg/modules/chromium/version_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestChromiumDetectVersion(t *testing.T) {
|
||||
t.Run("prefers the build-time version without executing Chromium", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "chromium"), []byte("Chromium 146.0.7680.80\n"), 0o600); err != nil {
|
||||
t.Fatalf("write version file: %v", err)
|
||||
}
|
||||
t.Setenv(gotenberg.BuildVersionsDirPathEnvVar, dir)
|
||||
|
||||
// A bogus binPath would error if executed, so a correct result proves
|
||||
// the build-time file is used instead of running Chromium.
|
||||
mod := &Chromium{args: browserArguments{binPath: "/nonexistent/chromium"}}
|
||||
if got := mod.Debug()["version"]; got != "Chromium 146.0.7680.80" {
|
||||
t.Errorf("Debug()[version] = %v, want the build-time value", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to executing Chromium when no build-time version", func(t *testing.T) {
|
||||
t.Setenv(gotenberg.BuildVersionsDirPathEnvVar, "")
|
||||
|
||||
// With no build-time file and a bogus binPath, the exec fallback runs
|
||||
// and records its error rather than a build-time value.
|
||||
mod := &Chromium{args: browserArguments{binPath: "/nonexistent/chromium"}}
|
||||
if got := mod.Debug()["version"]; got == "Chromium 146.0.7680.80" {
|
||||
t.Errorf("Debug()[version] = %v, expected the exec fallback", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -159,6 +161,9 @@ func buildExifToolWriteArgs(metadata map[string]any) ([]string, error) {
|
||||
// [gotenberg.PdfEngine] interface.
|
||||
type ExifTool struct {
|
||||
binPath string
|
||||
|
||||
version string
|
||||
versionOnce sync.Once
|
||||
}
|
||||
|
||||
// Descriptor returns [ExifTool]'s module descriptor.
|
||||
@@ -193,26 +198,53 @@ func (engine *ExifTool) Validate() error {
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *ExifTool) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
return map[string]any{"version": engine.detectVersion()}
|
||||
}
|
||||
|
||||
cmd := exec.Command(engine.binPath, "-ver") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
// detectVersion resolves the ExifTool version once, preferring the value
|
||||
// captured at image build time so it never spawns ExifTool at runtime. It falls
|
||||
// back to running exiftool -ver for local or non-Docker builds.
|
||||
func (engine *ExifTool) detectVersion() string {
|
||||
engine.versionOnce.Do(func() {
|
||||
if v, ok := gotenberg.BuildVersion("exiftool"); ok {
|
||||
engine.version = v
|
||||
return
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
cmd := exec.Command(engine.binPath, "-ver") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
engine.version = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
engine.version = strings.TrimSpace(string(output))
|
||||
})
|
||||
|
||||
return engine.version
|
||||
}
|
||||
|
||||
// spanAttrs returns the client-span attributes for an ExifTool invocation: the
|
||||
// server address and the ExifTool version, plus any extra attributes. The
|
||||
// version rides on every span so a trace records which ExifTool ran the
|
||||
// operation.
|
||||
func (engine *ExifTool) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
|
||||
attrs = append(attrs, semconv.ServerAddress(engine.binPath))
|
||||
if v := engine.detectVersion(); v != "" {
|
||||
attrs = append(attrs, attribute.String("gotenberg.exiftool.version", v))
|
||||
}
|
||||
|
||||
debug["version"] = strings.TrimSpace(string(output))
|
||||
return debug
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// Merge is not available in this implementation.
|
||||
func (engine *ExifTool) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Merge",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -226,7 +258,7 @@ func (engine *ExifTool) Merge(ctx context.Context, logger *slog.Logger, inputPat
|
||||
func (engine *ExifTool) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Split",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -240,7 +272,7 @@ func (engine *ExifTool) Split(ctx context.Context, logger *slog.Logger, mode got
|
||||
func (engine *ExifTool) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Flatten",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -254,7 +286,7 @@ func (engine *ExifTool) Flatten(ctx context.Context, logger *slog.Logger, inputP
|
||||
func (engine *ExifTool) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Convert",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -269,7 +301,7 @@ func (engine *ExifTool) Convert(ctx context.Context, logger *slog.Logger, format
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -326,7 +358,7 @@ func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *slog.Logger, i
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -371,7 +403,7 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger,
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -424,7 +456,7 @@ func (engine *ExifTool) PageCount(ctx context.Context, logger *slog.Logger, inpu
|
||||
func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteBookmarks",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -438,7 +470,7 @@ func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *slog.Logger,
|
||||
func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadBookmarks",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -452,7 +484,7 @@ func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *slog.Logger,
|
||||
func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -466,7 +498,7 @@ func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputP
|
||||
func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.EmbedFiles",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -480,7 +512,7 @@ func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *slog.Logger, fil
|
||||
func (engine *ExifTool) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Watermark",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -494,7 +526,7 @@ func (engine *ExifTool) Watermark(ctx context.Context, logger *slog.Logger, inpu
|
||||
func (engine *ExifTool) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Stamp",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -508,7 +540,7 @@ func (engine *ExifTool) Stamp(ctx context.Context, logger *slog.Logger, inputPat
|
||||
func (engine *ExifTool) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Rotate",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -53,6 +54,9 @@ type Api struct {
|
||||
libreOffice libreOffice
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
|
||||
version string
|
||||
versionOnce sync.Once
|
||||
|
||||
reqsCounter metric.Int64Counter
|
||||
errsCounter metric.Int64Counter
|
||||
conversionDurationCounter metric.Float64Histogram
|
||||
@@ -540,19 +544,46 @@ func (a *Api) Stop(ctx context.Context) error {
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (a *Api) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
return map[string]any{"version": a.detectVersion()}
|
||||
}
|
||||
|
||||
cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
// detectVersion resolves the LibreOffice version once, preferring the value
|
||||
// captured at image build time so it never spawns LibreOffice at runtime. It
|
||||
// falls back to running soffice --version for local or non-Docker builds.
|
||||
func (a *Api) detectVersion() string {
|
||||
a.versionOnce.Do(func() {
|
||||
if v, ok := gotenberg.BuildVersion("libreoffice-api"); ok {
|
||||
a.version = v
|
||||
return
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
a.version = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
a.version = strings.TrimSpace(string(output))
|
||||
})
|
||||
|
||||
return a.version
|
||||
}
|
||||
|
||||
// spanAttrs returns the client-span attributes for a LibreOffice invocation:
|
||||
// the server address and the LibreOffice version, plus any extra attributes.
|
||||
// The version rides on every conversion span so a trace records which
|
||||
// LibreOffice rendered the document.
|
||||
func (a *Api) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
|
||||
attrs = append(attrs, semconv.ServerAddress(a.args.binPath))
|
||||
if v := a.detectVersion(); v != "" {
|
||||
attrs = append(attrs, attribute.String("gotenberg.libreoffice.version", v))
|
||||
}
|
||||
|
||||
debug["version"] = strings.TrimSpace(string(output))
|
||||
return debug
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// Metrics returns the metrics.
|
||||
@@ -628,7 +659,7 @@ func (a *Api) LibreOffice() (Uno, 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(a.args.binPath)),
|
||||
trace.WithAttributes(a.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -30,6 +32,9 @@ func init() {
|
||||
// [gotenberg.PdfEngine] interface.
|
||||
type PdfCpu struct {
|
||||
binPath string
|
||||
|
||||
version string
|
||||
versionOnce sync.Once
|
||||
}
|
||||
|
||||
type pdfcpuBookmark struct {
|
||||
@@ -74,35 +79,60 @@ func (engine *PdfCpu) Validate() error {
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *PdfCpu) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
return map[string]any{"version": engine.detectVersion()}
|
||||
}
|
||||
|
||||
cmd := exec.Command(engine.binPath, "version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
}
|
||||
|
||||
debug["version"] = "Unable to determine pdfcpu version"
|
||||
|
||||
lines := strings.SplitSeq(string(output), "\n")
|
||||
for line := range lines {
|
||||
if after, ok := strings.CutPrefix(line, "pdfcpu:"); ok {
|
||||
debug["version"] = strings.TrimSpace(after)
|
||||
break
|
||||
// detectVersion resolves the pdfcpu version once, preferring the value captured
|
||||
// at image build time so it never spawns pdfcpu at runtime. It falls back to
|
||||
// running pdfcpu version for local or non-Docker builds.
|
||||
func (engine *PdfCpu) detectVersion() string {
|
||||
engine.versionOnce.Do(func() {
|
||||
if v, ok := gotenberg.BuildVersion("pdfcpu"); ok {
|
||||
engine.version = v
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(engine.binPath, "version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
engine.version = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
engine.version = "Unable to determine pdfcpu version"
|
||||
|
||||
lines := strings.SplitSeq(string(output), "\n")
|
||||
for line := range lines {
|
||||
if after, ok := strings.CutPrefix(line, "pdfcpu:"); ok {
|
||||
engine.version = strings.TrimSpace(after)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return engine.version
|
||||
}
|
||||
|
||||
// spanAttrs returns the client-span attributes for a pdfcpu invocation: the
|
||||
// server address and the pdfcpu version, plus any extra attributes. The version
|
||||
// rides on every span so a trace records which pdfcpu ran the operation.
|
||||
func (engine *PdfCpu) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
|
||||
attrs = append(attrs, semconv.ServerAddress(engine.binPath))
|
||||
if v := engine.detectVersion(); v != "" {
|
||||
attrs = append(attrs, attribute.String("gotenberg.pdfcpu.version", v))
|
||||
}
|
||||
|
||||
return debug
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// 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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -134,7 +164,7 @@ func (engine *PdfCpu) Merge(ctx context.Context, logger *slog.Logger, inputPaths
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -203,7 +233,7 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *slog.Logger, mode goten
|
||||
func (engine *PdfCpu) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Flatten",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -217,7 +247,7 @@ func (engine *PdfCpu) Flatten(ctx context.Context, logger *slog.Logger, inputPat
|
||||
func (engine *PdfCpu) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Convert",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -231,7 +261,7 @@ func (engine *PdfCpu) Convert(ctx context.Context, logger *slog.Logger, formats
|
||||
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -245,7 +275,7 @@ func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *slog.Logger, inp
|
||||
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -259,7 +289,7 @@ func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *slog.Logger, me
|
||||
func (engine *PdfCpu) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.PageCount",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -273,7 +303,7 @@ func (engine *PdfCpu) PageCount(ctx context.Context, logger *slog.Logger, inputP
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -376,7 +406,7 @@ func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, in
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -467,7 +497,7 @@ func (engine *PdfCpu) ReadPdfAConformance(ctx context.Context, logger *slog.Logg
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -506,7 +536,7 @@ func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *slog.Logger, fileP
|
||||
func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -561,7 +591,7 @@ func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPat
|
||||
func (engine *PdfCpu) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Watermark",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -580,7 +610,7 @@ func (engine *PdfCpu) Watermark(ctx context.Context, logger *slog.Logger, inputP
|
||||
func (engine *PdfCpu) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Stamp",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -599,7 +629,7 @@ func (engine *PdfCpu) Stamp(ctx context.Context, logger *slog.Logger, inputPath
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -26,6 +28,9 @@ func init() {
|
||||
// interface.
|
||||
type PdfTk struct {
|
||||
binPath string
|
||||
|
||||
version string
|
||||
versionOnce sync.Once
|
||||
}
|
||||
|
||||
// Descriptor returns a [PdfTk]'s module descriptor.
|
||||
@@ -60,32 +65,58 @@ func (engine *PdfTk) Validate() error {
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *PdfTk) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
return map[string]any{"version": engine.detectVersion()}
|
||||
}
|
||||
|
||||
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
// detectVersion resolves the PDFtk version once, preferring the value captured
|
||||
// at image build time so it never spawns the PDFtk JVM at runtime. It falls
|
||||
// back to running pdftk --version for local or non-Docker builds.
|
||||
func (engine *PdfTk) detectVersion() string {
|
||||
engine.versionOnce.Do(func() {
|
||||
if v, ok := gotenberg.BuildVersion("pdftk"); ok {
|
||||
engine.version = v
|
||||
return
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
engine.version = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
lines := bytes.SplitN(output, []byte("\n"), 2)
|
||||
if len(lines) > 0 {
|
||||
engine.version = string(lines[0])
|
||||
return
|
||||
}
|
||||
|
||||
engine.version = "Unable to determine PDFtk version"
|
||||
})
|
||||
|
||||
return engine.version
|
||||
}
|
||||
|
||||
// spanAttrs returns the client-span attributes for a PDFtk invocation: the
|
||||
// server address and the PDFtk version, plus any extra attributes. The version
|
||||
// rides on every span so a trace records which PDFtk ran the operation.
|
||||
func (engine *PdfTk) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
|
||||
attrs = append(attrs, semconv.ServerAddress(engine.binPath))
|
||||
if v := engine.detectVersion(); v != "" {
|
||||
attrs = append(attrs, attribute.String("gotenberg.pdftk.version", v))
|
||||
}
|
||||
|
||||
lines := bytes.SplitN(output, []byte("\n"), 2)
|
||||
if len(lines) > 0 {
|
||||
debug["version"] = string(lines[0])
|
||||
} else {
|
||||
debug["version"] = "Unable to determine PDFtk version"
|
||||
}
|
||||
|
||||
return debug
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// 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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -132,7 +163,7 @@ func (engine *PdfTk) Split(ctx context.Context, logger *slog.Logger, mode gotenb
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -164,7 +195,7 @@ func (engine *PdfTk) Merge(ctx context.Context, logger *slog.Logger, inputPaths
|
||||
func (engine *PdfTk) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.Flatten",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -178,7 +209,7 @@ func (engine *PdfTk) Flatten(ctx context.Context, logger *slog.Logger, inputPath
|
||||
func (engine *PdfTk) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.Convert",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -192,7 +223,7 @@ func (engine *PdfTk) Convert(ctx context.Context, logger *slog.Logger, formats g
|
||||
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -206,7 +237,7 @@ func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *slog.Logger, inpu
|
||||
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -220,7 +251,7 @@ func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *slog.Logger, met
|
||||
func (engine *PdfTk) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.PageCount",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -234,7 +265,7 @@ func (engine *PdfTk) PageCount(ctx context.Context, logger *slog.Logger, inputPa
|
||||
func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteBookmarks",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -248,7 +279,7 @@ func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *slog.Logger, in
|
||||
func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadBookmarks",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -262,7 +293,7 @@ func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *slog.Logger, inp
|
||||
func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -322,7 +353,7 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath
|
||||
func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.EmbedFiles",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -339,7 +370,7 @@ func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *slog.Logger, filePa
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -389,7 +420,7 @@ func (engine *PdfTk) Watermark(ctx context.Context, logger *slog.Logger, inputPa
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -438,7 +469,7 @@ func (engine *PdfTk) Stamp(ctx context.Context, logger *slog.Logger, inputPath s
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -33,6 +34,9 @@ func init() {
|
||||
type QPdf struct {
|
||||
binPath string
|
||||
globalArgs []string
|
||||
|
||||
version string
|
||||
versionOnce sync.Once
|
||||
}
|
||||
|
||||
// Descriptor returns a [QPdf]'s module descriptor.
|
||||
@@ -69,32 +73,58 @@ func (engine *QPdf) Validate() error {
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *QPdf) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
return map[string]any{"version": engine.detectVersion()}
|
||||
}
|
||||
|
||||
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
// detectVersion resolves the qpdf version once, preferring the value captured
|
||||
// at image build time so it never spawns qpdf at runtime. It falls back to
|
||||
// running qpdf --version for local or non-Docker builds.
|
||||
func (engine *QPdf) detectVersion() string {
|
||||
engine.versionOnce.Do(func() {
|
||||
if v, ok := gotenberg.BuildVersion("qpdf"); ok {
|
||||
engine.version = v
|
||||
return
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
engine.version = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
lines := bytes.SplitN(output, []byte("\n"), 2)
|
||||
if len(lines) > 0 {
|
||||
engine.version = string(lines[0])
|
||||
return
|
||||
}
|
||||
|
||||
engine.version = "Unable to determine QPDF version"
|
||||
})
|
||||
|
||||
return engine.version
|
||||
}
|
||||
|
||||
// spanAttrs returns the client-span attributes for a qpdf invocation: the
|
||||
// server address and the qpdf version, plus any extra attributes. The version
|
||||
// rides on every span so a trace records which qpdf ran the operation.
|
||||
func (engine *QPdf) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
|
||||
attrs = append(attrs, semconv.ServerAddress(engine.binPath))
|
||||
if v := engine.detectVersion(); v != "" {
|
||||
attrs = append(attrs, attribute.String("gotenberg.qpdf.version", v))
|
||||
}
|
||||
|
||||
lines := bytes.SplitN(output, []byte("\n"), 2)
|
||||
if len(lines) > 0 {
|
||||
debug["version"] = string(lines[0])
|
||||
} else {
|
||||
debug["version"] = "Unable to determine QPDF version"
|
||||
}
|
||||
|
||||
return debug
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// 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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -144,7 +174,7 @@ func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenbe
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -180,7 +210,7 @@ func (engine *QPdf) Merge(ctx context.Context, logger *slog.Logger, inputPaths [
|
||||
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(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -215,7 +245,7 @@ func (engine *QPdf) Flatten(ctx context.Context, logger *slog.Logger, inputPath
|
||||
func (engine *QPdf) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Convert",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -229,7 +259,7 @@ func (engine *QPdf) Convert(ctx context.Context, logger *slog.Logger, formats go
|
||||
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -243,7 +273,7 @@ func (engine *QPdf) ReadMetadata(ctx context.Context, logger *slog.Logger, input
|
||||
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -257,7 +287,7 @@ func (engine *QPdf) WriteMetadata(ctx context.Context, logger *slog.Logger, meta
|
||||
func (engine *QPdf) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.PageCount",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -271,7 +301,7 @@ func (engine *QPdf) PageCount(ctx context.Context, logger *slog.Logger, inputPat
|
||||
func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteBookmarks",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -285,7 +315,7 @@ func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *slog.Logger, inp
|
||||
func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadBookmarks",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -329,7 +359,7 @@ func qpdfPermissionArgs(p gotenberg.PdfPermissions) []string {
|
||||
func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -378,7 +408,7 @@ func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath
|
||||
func (engine *QPdf) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFiles",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -395,7 +425,7 @@ func (engine *QPdf) EmbedFiles(ctx context.Context, logger *slog.Logger, filePat
|
||||
func (engine *QPdf) EmbedFilesMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]map[string]string, inputPath string) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFilesMetadata",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -692,11 +722,10 @@ const facturXNamespaceURI = "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0
|
||||
func (engine *QPdf) InjectFacturXXMP(ctx context.Context, logger *slog.Logger, facturX gotenberg.FacturX, inputPath string) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.InjectFacturXXMP",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
semconv.ServerAddress(engine.binPath),
|
||||
trace.WithAttributes(engine.spanAttrs(
|
||||
attribute.String("gotenberg.facturx.conformance_level", facturX.ConformanceLevel),
|
||||
attribute.String("gotenberg.facturx.document_type", facturX.DocumentType),
|
||||
),
|
||||
)...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -776,7 +805,7 @@ func (engine *QPdf) InjectFacturXXMP(ctx context.Context, logger *slog.Logger, f
|
||||
func (engine *QPdf) ReadPdfAConformance(ctx context.Context, logger *slog.Logger, inputPath string) (string, string, error) {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadPdfAConformance",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -1066,7 +1095,7 @@ func xmlEscape(s string) string {
|
||||
func (engine *QPdf) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Watermark",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -1080,7 +1109,7 @@ func (engine *QPdf) Watermark(ctx context.Context, logger *slog.Logger, inputPat
|
||||
func (engine *QPdf) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Stamp",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
@@ -1094,7 +1123,7 @@ func (engine *QPdf) Stamp(ctx context.Context, logger *slog.Logger, inputPath st
|
||||
func (engine *QPdf) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Rotate",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
trace.WithAttributes(engine.spanAttrs()...),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
|
||||
58
pkg/modules/qpdf/version_test.go
Normal file
58
pkg/modules/qpdf/version_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package qpdf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestQPdfDetectVersion(t *testing.T) {
|
||||
t.Run("prefers the build-time version without executing qpdf", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "qpdf"), []byte("qpdf version 11.9.0\n"), 0o600); err != nil {
|
||||
t.Fatalf("write version file: %v", err)
|
||||
}
|
||||
t.Setenv(gotenberg.BuildVersionsDirPathEnvVar, dir)
|
||||
|
||||
// A bogus binPath would error if executed, so a correct result proves
|
||||
// the build-time file is used instead of running qpdf.
|
||||
engine := &QPdf{binPath: "/nonexistent/qpdf"}
|
||||
if got := engine.Debug()["version"]; got != "qpdf version 11.9.0" {
|
||||
t.Errorf("Debug()[version] = %v, want the build-time value", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to executing qpdf when no build-time version", func(t *testing.T) {
|
||||
t.Setenv(gotenberg.BuildVersionsDirPathEnvVar, "")
|
||||
|
||||
// With no build-time file and a bogus binPath, the exec fallback runs
|
||||
// and records its error rather than a build-time value.
|
||||
engine := &QPdf{binPath: "/nonexistent/qpdf"}
|
||||
if got := engine.Debug()["version"]; got == "qpdf version 11.9.0" {
|
||||
t.Errorf("Debug()[version] = %v, expected the exec fallback", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exposes the version as a span attribute", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "qpdf"), []byte("qpdf version 12.2.0"), 0o600); err != nil {
|
||||
t.Fatalf("write version file: %v", err)
|
||||
}
|
||||
t.Setenv(gotenberg.BuildVersionsDirPathEnvVar, dir)
|
||||
|
||||
// spanAttrs is what gets handed to trace.WithAttributes, so its output
|
||||
// is exactly what a span carries.
|
||||
engine := &QPdf{binPath: "/nonexistent/qpdf"}
|
||||
var got string
|
||||
for _, attr := range engine.spanAttrs() {
|
||||
if attr.Key == "gotenberg.qpdf.version" {
|
||||
got = attr.Value.AsString()
|
||||
}
|
||||
}
|
||||
if got != "qpdf version 12.2.0" {
|
||||
t.Errorf("span attribute gotenberg.qpdf.version = %q, want the build-time value", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user