mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
feat(telemetry): record backing-binary versions on spans, captured at build time
This commit is contained in:
@@ -190,6 +190,19 @@ ENV QPDF_BIN_PATH=/usr/bin/qpdf
|
|||||||
ENV EXIFTOOL_BIN_PATH=/usr/bin/exiftool
|
ENV EXIFTOOL_BIN_PATH=/usr/bin/exiftool
|
||||||
ENV PDFCPU_BIN_PATH=/usr/bin/pdfcpu
|
ENV PDFCPU_BIN_PATH=/usr/bin/pdfcpu
|
||||||
|
|
||||||
|
# Capture backing-binary versions at build time so the running process reports
|
||||||
|
# them on traces without spawning the binaries at startup or per request.
|
||||||
|
# See pkg/gotenberg/buildversions.go. Chromium and LibreOffice are captured in
|
||||||
|
# the variant stages below, where they are installed.
|
||||||
|
ENV GOTENBERG_VERSIONS_DIR_PATH=/opt/gotenberg/versions
|
||||||
|
|
||||||
|
COPY --link build/capture-version.sh /opt/gotenberg/capture-version.sh
|
||||||
|
|
||||||
|
RUN bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" qpdf "$QPDF_BIN_PATH" --version \
|
||||||
|
&& bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" exiftool "$EXIFTOOL_BIN_PATH" -ver \
|
||||||
|
&& bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" pdftk "$PDFTK_BIN_PATH" --version \
|
||||||
|
&& bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" pdfcpu "$PDFCPU_BIN_PATH" version
|
||||||
|
|
||||||
# OpenTelemetry defaults (noop - no telemetry overhead unless explicitly enabled).
|
# OpenTelemetry defaults (noop - no telemetry overhead unless explicitly enabled).
|
||||||
ENV OTEL_TRACES_EXPORTER=none
|
ENV OTEL_TRACES_EXPORTER=none
|
||||||
ENV OTEL_METRICS_EXPORTER=none
|
ENV OTEL_METRICS_EXPORTER=none
|
||||||
@@ -269,6 +282,10 @@ ENV CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data
|
|||||||
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
|
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
|
||||||
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
|
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
|
||||||
|
|
||||||
|
# Capture Chromium and LibreOffice versions now that both are installed.
|
||||||
|
RUN bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" chromium "$CHROMIUM_BIN_PATH" --version \
|
||||||
|
&& bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" libreoffice-api "$LIBREOFFICE_BIN_PATH" --version
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|
||||||
@@ -329,6 +346,9 @@ ENV CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data
|
|||||||
# No LibreOffice in this variant; override the default to use all available engines.
|
# No LibreOffice in this variant; override the default to use all available engines.
|
||||||
ENV PDFENGINES_CONVERT_ENGINES=
|
ENV PDFENGINES_CONVERT_ENGINES=
|
||||||
|
|
||||||
|
# Capture the Chromium version now that it is installed.
|
||||||
|
RUN bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" chromium "$CHROMIUM_BIN_PATH" --version
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|
||||||
@@ -383,6 +403,9 @@ COPY --link --from=downloader-stage /downloads/unoconverter /usr/bin/unoconverte
|
|||||||
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
|
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
|
||||||
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
|
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
|
||||||
|
|
||||||
|
# Capture the LibreOffice version now that it is installed.
|
||||||
|
RUN bash /opt/gotenberg/capture-version.sh "$GOTENBERG_VERSIONS_DIR_PATH" libreoffice-api "$LIBREOFFICE_BIN_PATH" --version
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Captures the version of a backing binary into a per-module file that the
|
||||||
|
# running Gotenberg process reads via gotenberg.BuildVersion, so it never spawns
|
||||||
|
# the binary just to report a version. This keeps cold start and the first
|
||||||
|
# request cheap, which matters on serverless platforms.
|
||||||
|
#
|
||||||
|
# Failure-tolerant by design: a probe that errors writes an empty file, and the
|
||||||
|
# runtime falls back to detecting the version live. A failing probe must never
|
||||||
|
# fail the image build.
|
||||||
|
#
|
||||||
|
# Usage: capture-version.sh <output-dir> <module-id> <bin> [args...]
|
||||||
|
set -u
|
||||||
|
|
||||||
|
dir="$1"
|
||||||
|
id="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
mkdir -p "$dir"
|
||||||
|
|
||||||
|
# Run the probe once. On failure, keep going with empty output.
|
||||||
|
raw="$("$@" 2>/dev/null)" || raw=""
|
||||||
|
|
||||||
|
case "$id" in
|
||||||
|
pdfcpu)
|
||||||
|
# pdfcpu prints "pdfcpu: <version>"; keep only the part the runtime parser
|
||||||
|
# keeps so the recorded value matches the live-detection fallback.
|
||||||
|
version="$(printf '%s\n' "$raw" | grep -m1 '^pdfcpu:' | sed 's/^pdfcpu:[[:space:]]*//')"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
version="$(printf '%s\n' "$raw" | head -n1)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf '%s' "$version" | tr -d '\r' >"$dir/$id"
|
||||||
@@ -194,9 +194,6 @@ func Run() {
|
|||||||
if parsedFlags.MustBool("gotenberg-build-debug-data") {
|
if parsedFlags.MustBool("gotenberg-build-debug-data") {
|
||||||
// Build the debug data.
|
// Build the debug data.
|
||||||
gotenberg.BuildDebug(ctx)
|
gotenberg.BuildDebug(ctx)
|
||||||
|
|
||||||
// Surface engine versions per trace once modules have reported them.
|
|
||||||
gotenberg.EmitStartupSpan(context.Background())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildVersionsDirPathEnvVar names the environment variable holding the
|
||||||
|
// absolute path to a directory of build-time version files. The Gotenberg image
|
||||||
|
// writes one file per module there, named by module ID and holding the version
|
||||||
|
// string of that module's backing binary, captured right after the binary is
|
||||||
|
// installed. The running process reads these files instead of executing the
|
||||||
|
// binaries, which keeps startup and the first request cheap.
|
||||||
|
const BuildVersionsDirPathEnvVar = "GOTENBERG_VERSIONS_DIR_PATH"
|
||||||
|
|
||||||
|
// BuildVersion returns the build-time version captured for the module with the
|
||||||
|
// given ID. The boolean is false when no version was captured, which is the
|
||||||
|
// case for local or non-Docker builds where the directory is absent. A module
|
||||||
|
// uses it to avoid spawning its backing binary just to report a version.
|
||||||
|
//
|
||||||
|
// It is defensive: an unset variable, a missing or unreadable file, or an empty
|
||||||
|
// value all yield ("", false), so the caller falls back to detecting the
|
||||||
|
// version at runtime. See [BuildVersionsDirPathEnvVar].
|
||||||
|
func BuildVersion(moduleID string) (string, bool) {
|
||||||
|
dir := os.Getenv(BuildVersionsDirPathEnvVar)
|
||||||
|
if dir == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module IDs are fixed internal constants, never paths. Guard anyway so a
|
||||||
|
// stray separator can't escape the versions directory.
|
||||||
|
if moduleID != filepath.Base(moduleID) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The directory comes from a trusted operator-set environment variable,
|
||||||
|
// mirroring how engines exec their env-configured binaries.
|
||||||
|
b, err := os.ReadFile(filepath.Join(dir, moduleID)) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
version := strings.TrimSpace(string(b))
|
||||||
|
if version == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return version, true
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildVersion(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
fileBody string
|
||||||
|
writeFile bool
|
||||||
|
setEnv bool
|
||||||
|
moduleID string
|
||||||
|
wantValue string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "version present",
|
||||||
|
fileBody: "Chromium 146.0",
|
||||||
|
writeFile: true,
|
||||||
|
setEnv: true,
|
||||||
|
moduleID: "chromium",
|
||||||
|
wantValue: "Chromium 146.0",
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "value trimmed",
|
||||||
|
fileBody: " qpdf version 11.9.0 \n",
|
||||||
|
writeFile: true,
|
||||||
|
setEnv: true,
|
||||||
|
moduleID: "qpdf",
|
||||||
|
wantValue: "qpdf version 11.9.0",
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "empty file falls back",
|
||||||
|
fileBody: " \n",
|
||||||
|
writeFile: true,
|
||||||
|
setEnv: true,
|
||||||
|
moduleID: "pdftk",
|
||||||
|
wantValue: "",
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "missing file falls back",
|
||||||
|
writeFile: false,
|
||||||
|
setEnv: true,
|
||||||
|
moduleID: "exiftool",
|
||||||
|
wantValue: "",
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "env unset falls back",
|
||||||
|
setEnv: false,
|
||||||
|
moduleID: "chromium",
|
||||||
|
wantValue: "",
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.setEnv {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if tc.writeFile {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, tc.moduleID), []byte(tc.fileBody), 0o600); err != nil {
|
||||||
|
t.Fatalf("write version file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Setenv(BuildVersionsDirPathEnvVar, dir)
|
||||||
|
} else {
|
||||||
|
t.Setenv(BuildVersionsDirPathEnvVar, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := BuildVersion(tc.moduleID)
|
||||||
|
if value != tc.wantValue || ok != tc.wantOk {
|
||||||
|
t.Errorf("BuildVersion(%q) = (%q, %t), want (%q, %t)", tc.moduleID, value, ok, tc.wantValue, tc.wantOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDebugModuleVersion(t *testing.T) {
|
|
||||||
info := DebugInfo{
|
|
||||||
ModulesAdditionalData: map[string]map[string]any{
|
|
||||||
"chromium": {"version": "Chromium 145.0"},
|
|
||||||
"broken": {"version": 42},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := debugModuleVersion(info, "chromium"); got != "Chromium 145.0" {
|
|
||||||
t.Errorf("expected chromium version, got %q", got)
|
|
||||||
}
|
|
||||||
if got := debugModuleVersion(info, "missing"); got != "" {
|
|
||||||
t.Errorf("expected empty for a missing module, got %q", got)
|
|
||||||
}
|
|
||||||
if got := debugModuleVersion(info, "broken"); got != "" {
|
|
||||||
t.Errorf("expected empty for a non-string version, got %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmitStartupSpan(t *testing.T) {
|
|
||||||
recorder := newTestSpanRecorder(t)
|
|
||||||
|
|
||||||
debugMu.Lock()
|
|
||||||
previous := debug
|
|
||||||
debug = &DebugInfo{
|
|
||||||
Version: "v8.0.0",
|
|
||||||
ModulesAdditionalData: map[string]map[string]any{
|
|
||||||
"chromium": {"version": "Chromium 145.0"},
|
|
||||||
"libreoffice-api": {"version": "LibreOffice 24.8"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
debugMu.Unlock()
|
|
||||||
t.Cleanup(func() {
|
|
||||||
debugMu.Lock()
|
|
||||||
debug = previous
|
|
||||||
debugMu.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
EmitStartupSpan(context.Background())
|
|
||||||
|
|
||||||
span := findSpan(recorder, "gotenberg.startup")
|
|
||||||
if span == nil {
|
|
||||||
t.Fatal("expected a gotenberg.startup span to be recorded")
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := spanAttr(span, "gotenberg.chromium.version"); !ok || v.AsString() != "Chromium 145.0" {
|
|
||||||
t.Errorf("expected gotenberg.chromium.version=Chromium 145.0, got %q (present=%t)", v.AsString(), ok)
|
|
||||||
}
|
|
||||||
if v, ok := spanAttr(span, "gotenberg.libreoffice.version"); !ok || v.AsString() != "LibreOffice 24.8" {
|
|
||||||
t.Errorf("expected gotenberg.libreoffice.version=LibreOffice 24.8, got %q (present=%t)", v.AsString(), ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/go-retryablehttp"
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
@@ -205,46 +204,6 @@ func Meter() metric.Meter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmitStartupSpan records a single gotenberg.startup span carrying static,
|
|
||||||
// process-wide attributes that are only known once modules are provisioned,
|
|
||||||
// such as the chromium and libreoffice binary versions gathered by
|
|
||||||
// [BuildDebug]. It surfaces version data per trace without re-detecting it on
|
|
||||||
// every conversion. The engine versions live here, on a span, rather than on
|
|
||||||
// the resource because the resource is built before modules report them.
|
|
||||||
func EmitStartupSpan(ctx context.Context) {
|
|
||||||
info := Debug()
|
|
||||||
|
|
||||||
var attrs []attribute.KeyValue
|
|
||||||
if v := debugModuleVersion(info, "chromium"); v != "" {
|
|
||||||
attrs = append(attrs, attribute.String("gotenberg.chromium.version", v))
|
|
||||||
}
|
|
||||||
if v := debugModuleVersion(info, "libreoffice-api"); v != "" {
|
|
||||||
attrs = append(attrs, attribute.String("gotenberg.libreoffice.version", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, span := Tracer().Start(ctx, "gotenberg.startup",
|
|
||||||
trace.WithSpanKind(trace.SpanKindInternal),
|
|
||||||
trace.WithAttributes(attrs...),
|
|
||||||
)
|
|
||||||
span.End()
|
|
||||||
}
|
|
||||||
|
|
||||||
// debugModuleVersion returns the "version" entry reported by the module with
|
|
||||||
// the given ID, or an empty string when it is missing.
|
|
||||||
func debugModuleVersion(info DebugInfo, moduleID string) string {
|
|
||||||
data, ok := info.ModulesAdditionalData[moduleID]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
version, ok := data["version"].(string)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return version
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeveledLogger is a wrapper around a [slog.Logger] so that it may be used by a
|
// LeveledLogger is a wrapper around a [slog.Logger] so that it may be used by a
|
||||||
// [retryablehttp.Client].
|
// [retryablehttp.Client].
|
||||||
type LeveledLogger struct {
|
type LeveledLogger struct {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -101,6 +102,9 @@ type Chromium struct {
|
|||||||
supervisor gotenberg.ProcessSupervisor
|
supervisor gotenberg.ProcessSupervisor
|
||||||
engine gotenberg.PdfEngine
|
engine gotenberg.PdfEngine
|
||||||
|
|
||||||
|
version string
|
||||||
|
versionOnce sync.Once
|
||||||
|
|
||||||
reqsCounter metric.Int64Counter
|
reqsCounter metric.Int64Counter
|
||||||
errsCounter metric.Int64Counter
|
errsCounter metric.Int64Counter
|
||||||
conversionDurationCounter metric.Float64Histogram
|
conversionDurationCounter metric.Float64Histogram
|
||||||
@@ -717,19 +721,46 @@ func (mod *Chromium) Stop(ctx context.Context) error {
|
|||||||
|
|
||||||
// Debug returns additional debug data.
|
// Debug returns additional debug data.
|
||||||
func (mod *Chromium) Debug() map[string]any {
|
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
|
// detectVersion resolves the Chromium version once, preferring the value
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
// 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()
|
cmd := exec.Command(mod.args.binPath, "--version") //nolint:gosec
|
||||||
if err != nil {
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
debug["version"] = err.Error()
|
|
||||||
return debug
|
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 append(attrs, extra...)
|
||||||
return debug
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics returns the metrics.
|
// 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",
|
ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Pdf",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(mod.args.binPath)),
|
trace.WithAttributes(mod.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (mod *Chromium) Screenshot(ctx context.Context, logger *slog.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Screenshot",
|
ctx, span := gotenberg.Tracer().Start(ctx, "chromium.Screenshot",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(mod.args.binPath)),
|
trace.WithAttributes(mod.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
@@ -159,6 +161,9 @@ func buildExifToolWriteArgs(metadata map[string]any) ([]string, error) {
|
|||||||
// [gotenberg.PdfEngine] interface.
|
// [gotenberg.PdfEngine] interface.
|
||||||
type ExifTool struct {
|
type ExifTool struct {
|
||||||
binPath string
|
binPath string
|
||||||
|
|
||||||
|
version string
|
||||||
|
versionOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns [ExifTool]'s module descriptor.
|
// Descriptor returns [ExifTool]'s module descriptor.
|
||||||
@@ -193,26 +198,53 @@ func (engine *ExifTool) Validate() error {
|
|||||||
|
|
||||||
// Debug returns additional debug data.
|
// Debug returns additional debug data.
|
||||||
func (engine *ExifTool) Debug() map[string]any {
|
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
|
// detectVersion resolves the ExifTool version once, preferring the value
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
// 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()
|
cmd := exec.Command(engine.binPath, "-ver") //nolint:gosec
|
||||||
if err != nil {
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
debug["version"] = err.Error()
|
|
||||||
return debug
|
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 append(attrs, extra...)
|
||||||
return debug
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge is not available in this implementation.
|
// Merge is not available in this implementation.
|
||||||
func (engine *ExifTool) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
func (engine *ExifTool) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Merge",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Merge",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
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",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Split",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Flatten",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Flatten",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Convert",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Convert",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *ExifTool) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.PageCount",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.PageCount",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteBookmarks",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.WriteBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadBookmarks",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.ReadBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Encrypt",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Encrypt",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.EmbedFiles",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.EmbedFiles",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Watermark",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Watermark",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Stamp",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Stamp",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *ExifTool) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Rotate",
|
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Rotate",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ type Api struct {
|
|||||||
libreOffice libreOffice
|
libreOffice libreOffice
|
||||||
supervisor gotenberg.ProcessSupervisor
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
|
||||||
|
version string
|
||||||
|
versionOnce sync.Once
|
||||||
|
|
||||||
reqsCounter metric.Int64Counter
|
reqsCounter metric.Int64Counter
|
||||||
errsCounter metric.Int64Counter
|
errsCounter metric.Int64Counter
|
||||||
conversionDurationCounter metric.Float64Histogram
|
conversionDurationCounter metric.Float64Histogram
|
||||||
@@ -540,19 +544,46 @@ func (a *Api) Stop(ctx context.Context) error {
|
|||||||
|
|
||||||
// Debug returns additional debug data.
|
// Debug returns additional debug data.
|
||||||
func (a *Api) Debug() map[string]any {
|
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
|
// detectVersion resolves the LibreOffice version once, preferring the value
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
// 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()
|
cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
|
||||||
if err != nil {
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
debug["version"] = err.Error()
|
|
||||||
return debug
|
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 append(attrs, extra...)
|
||||||
return debug
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics returns the metrics.
|
// 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 {
|
func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "libreoffice.Pdf",
|
ctx, span := gotenberg.Tracer().Start(ctx, "libreoffice.Pdf",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(a.args.binPath)),
|
trace.WithAttributes(a.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
@@ -30,6 +32,9 @@ func init() {
|
|||||||
// [gotenberg.PdfEngine] interface.
|
// [gotenberg.PdfEngine] interface.
|
||||||
type PdfCpu struct {
|
type PdfCpu struct {
|
||||||
binPath string
|
binPath string
|
||||||
|
|
||||||
|
version string
|
||||||
|
versionOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
type pdfcpuBookmark struct {
|
type pdfcpuBookmark struct {
|
||||||
@@ -74,35 +79,60 @@ func (engine *PdfCpu) Validate() error {
|
|||||||
|
|
||||||
// Debug returns additional debug data.
|
// Debug returns additional debug data.
|
||||||
func (engine *PdfCpu) Debug() map[string]any {
|
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
|
// detectVersion resolves the pdfcpu version once, preferring the value captured
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
// at image build time so it never spawns pdfcpu at runtime. It falls back to
|
||||||
|
// running pdfcpu version for local or non-Docker builds.
|
||||||
output, err := cmd.Output()
|
func (engine *PdfCpu) detectVersion() string {
|
||||||
if err != nil {
|
engine.versionOnce.Do(func() {
|
||||||
debug["version"] = err.Error()
|
if v, ok := gotenberg.BuildVersion("pdfcpu"); ok {
|
||||||
return debug
|
engine.version = v
|
||||||
}
|
return
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
// Merge combines multiple PDFs into a single PDF.
|
||||||
func (engine *PdfCpu) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfCpu) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Merge",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Merge",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
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",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Split",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Flatten",
|
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Flatten",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Convert",
|
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Convert",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *PdfCpu) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.PageCount",
|
_, span := gotenberg.Tracer().Start(ctx, "pdfcpu.PageCount",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadBookmarks",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.ReadBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteBookmarks",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.WriteBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.EmbedFiles",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.EmbedFiles",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Encrypt",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Encrypt",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Watermark",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Watermark",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfCpu) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Stamp",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Stamp",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
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",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Rotate",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
+60
-29
@@ -9,8 +9,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
@@ -26,6 +28,9 @@ func init() {
|
|||||||
// interface.
|
// interface.
|
||||||
type PdfTk struct {
|
type PdfTk struct {
|
||||||
binPath string
|
binPath string
|
||||||
|
|
||||||
|
version string
|
||||||
|
versionOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a [PdfTk]'s module descriptor.
|
// Descriptor returns a [PdfTk]'s module descriptor.
|
||||||
@@ -60,32 +65,58 @@ func (engine *PdfTk) Validate() error {
|
|||||||
|
|
||||||
// Debug returns additional debug data.
|
// Debug returns additional debug data.
|
||||||
func (engine *PdfTk) Debug() map[string]any {
|
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
|
// detectVersion resolves the PDFtk version once, preferring the value captured
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
// 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()
|
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||||
if err != nil {
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
debug["version"] = err.Error()
|
|
||||||
return debug
|
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)
|
return append(attrs, extra...)
|
||||||
if len(lines) > 0 {
|
|
||||||
debug["version"] = string(lines[0])
|
|
||||||
} else {
|
|
||||||
debug["version"] = "Unable to determine PDFtk version"
|
|
||||||
}
|
|
||||||
|
|
||||||
return debug
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split splits a given PDF file.
|
// Split splits a given PDF file.
|
||||||
func (engine *PdfTk) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
func (engine *PdfTk) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Split",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Split",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Merge",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Merge",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.Flatten",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.Flatten",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.Convert",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.Convert",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *PdfTk) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.PageCount",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.PageCount",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteBookmarks",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.WriteBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadBookmarks",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.ReadBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Encrypt",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Encrypt",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "pdftk.EmbedFiles",
|
_, span := gotenberg.Tracer().Start(ctx, "pdftk.EmbedFiles",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Watermark",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Watermark",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *PdfTk) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Stamp",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Stamp",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
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",
|
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Rotate",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
+63
-34
@@ -14,6 +14,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
@@ -33,6 +34,9 @@ func init() {
|
|||||||
type QPdf struct {
|
type QPdf struct {
|
||||||
binPath string
|
binPath string
|
||||||
globalArgs []string
|
globalArgs []string
|
||||||
|
|
||||||
|
version string
|
||||||
|
versionOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a [QPdf]'s module descriptor.
|
// Descriptor returns a [QPdf]'s module descriptor.
|
||||||
@@ -69,32 +73,58 @@ func (engine *QPdf) Validate() error {
|
|||||||
|
|
||||||
// Debug returns additional debug data.
|
// Debug returns additional debug data.
|
||||||
func (engine *QPdf) Debug() map[string]any {
|
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
|
// detectVersion resolves the qpdf version once, preferring the value captured
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
// 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()
|
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||||
if err != nil {
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
debug["version"] = err.Error()
|
|
||||||
return debug
|
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)
|
return append(attrs, extra...)
|
||||||
if len(lines) > 0 {
|
|
||||||
debug["version"] = string(lines[0])
|
|
||||||
} else {
|
|
||||||
debug["version"] = "Unable to determine QPDF version"
|
|
||||||
}
|
|
||||||
|
|
||||||
return debug
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split splits a given PDF file.
|
// Split splits a given PDF file.
|
||||||
func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Split",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Split",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Merge",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Merge",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Flatten",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Flatten",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Convert",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Convert",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteMetadata",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *QPdf) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.PageCount",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.PageCount",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteBookmarks",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.WriteBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadBookmarks",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadBookmarks",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Encrypt",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Encrypt",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFiles",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFiles",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
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",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFilesMetadata",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) InjectFacturXXMP(ctx context.Context, logger *slog.Logger, facturX gotenberg.FacturX, inputPath string) error {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.InjectFacturXXMP",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.InjectFacturXXMP",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(
|
trace.WithAttributes(engine.spanAttrs(
|
||||||
semconv.ServerAddress(engine.binPath),
|
|
||||||
attribute.String("gotenberg.facturx.conformance_level", facturX.ConformanceLevel),
|
attribute.String("gotenberg.facturx.conformance_level", facturX.ConformanceLevel),
|
||||||
attribute.String("gotenberg.facturx.document_type", facturX.DocumentType),
|
attribute.String("gotenberg.facturx.document_type", facturX.DocumentType),
|
||||||
),
|
)...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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) {
|
func (engine *QPdf) ReadPdfAConformance(ctx context.Context, logger *slog.Logger, inputPath string) (string, string, error) {
|
||||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadPdfAConformance",
|
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.ReadPdfAConformance",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Watermark",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Watermark",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Stamp",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Stamp",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
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 {
|
func (engine *QPdf) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
|
||||||
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Rotate",
|
_, span := gotenberg.Tracer().Start(ctx, "qpdf.Rotate",
|
||||||
trace.WithSpanKind(trace.SpanKindClient),
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
trace.WithAttributes(engine.spanAttrs()...),
|
||||||
)
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
@@ -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