diff --git a/README.md b/README.md index 16209d1..bc13467 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,10 @@ for the supported methods of providing the credentials. Launch the binary to start the webserver at `http://localhost:3000`. -The webserver provides two endpoints: +The webserver provides three endpoints: - `/v1/print` stores the generated PDF in an AWS S3 bucket - `/v2/print` streams the generated PDF as the response +- `/metrics` exports metrics in the Prometheus format Both endpoints accept `POST` requests with the following body parameters: - `url` (**required**) the URL of the page to print as PDF diff --git a/go.work.sum b/go.work.sum index a1880cc..137dbad 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,7 +1,37 @@ +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/lambda/go.mod b/lambda/go.mod index 594affb..5d949bb 100644 --- a/lambda/go.mod +++ b/lambda/go.mod @@ -36,5 +36,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/sys v0.25.0 // indirect ) diff --git a/lambda/go.sum b/lambda/go.sum index 65c6d63..7215366 100644 --- a/lambda/go.sum +++ b/lambda/go.sum @@ -67,8 +67,8 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= diff --git a/lambda/handlers.go b/lambda/handlers.go new file mode 100644 index 0000000..8b9777c --- /dev/null +++ b/lambda/handlers.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "slices" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/chialab/print2pdf-go/print2pdf" +) + +// Handle a request. +func handler(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + headers := map[string]string{"Content-Type": "application/json"} + if CorsAllowedHosts == "" || CorsAllowedHosts == "*" { + headers["Access-Control-Allow-Origin"] = "*" + } else { + allowedHosts := strings.Split(CorsAllowedHosts, ",") + origin := event.Headers["Origin"] + if origin == "" { + origin = event.Headers["origin"] + } + if slices.Contains(allowedHosts, origin) { + headers["Access-Control-Allow-Origin"] = origin + } + } + + var data print2pdf.GetPDFParams + err := json.Unmarshal([]byte(event.Body), &data) + if err != nil { + fmt.Fprintf(os.Stderr, "error decoding JSON: %s\noriginal request body: %s\n", err, event.Body) + + return jsonError("internal server error", 500), nil + } + if !strings.HasSuffix(data.FileName, ".pdf") { + data.FileName += ".pdf" + } + + h, err := print2pdf.NewS3Handler(ctx, BucketName, data.FileName) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating print handler: %s\n", err) + + return jsonError("internal server error", 500), nil + } + + url, err := print2pdf.PrintPDF(ctx, data, h) + if ve, ok := err.(print2pdf.ValidationError); ok { + fmt.Fprintf(os.Stderr, "request validation error: %s\n", ve) + + return jsonError(ve.Error(), 400), nil + } else if err != nil { + fmt.Fprintf(os.Stderr, "error getting PDF: %s\n", err) + + return jsonError("internal server error", 500), nil + } + + body, err := json.Marshal(ResponseData{Url: url}) + if err != nil { + fmt.Fprintf(os.Stderr, "error encoding response to JSON: %s\n", err) + + return jsonError("internal server error", 500), nil + } + + return events.APIGatewayProxyResponse{ + StatusCode: 200, + Body: string(body), + Headers: headers, + }, nil +} + +// Prepare an HTTP error response. +func jsonError(message string, code int) events.APIGatewayProxyResponse { + ct := "application/json" + body, err := json.Marshal(ResponseError{message}) + if err != nil { + fmt.Fprintf(os.Stderr, "error encoding error message to JSON: %s\noriginal error: %s\n", err, message) + body = []byte("internal server error") + code = 500 + ct = "text/plain" + } + + return events.APIGatewayProxyResponse{ + StatusCode: code, + Body: string(body), + Headers: map[string]string{ + "Content-Type": ct, + "X-Content-Type-Options": "nosniff", + }, + } +} diff --git a/lambda/main.go b/lambda/main.go index 71eec21..bca768b 100644 --- a/lambda/main.go +++ b/lambda/main.go @@ -2,15 +2,12 @@ package main import ( "context" - "encoding/json" "fmt" "os" "os/signal" "slices" - "strings" "syscall" - "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/chialab/print2pdf-go/print2pdf" ) @@ -47,95 +44,24 @@ func init() { } func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - if err := print2pdf.StartBrowser(ctx); err != nil { - fmt.Fprintf(os.Stderr, "error starting browser: %s\n", err) + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(1) } - - lambda.Start(handler) - - <-ctx.Done() - stop() } -// Handle a request. -func handler(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - headers := map[string]string{"Content-Type": "application/json"} - if CorsAllowedHosts == "" || CorsAllowedHosts == "*" { - headers["Access-Control-Allow-Origin"] = "*" - } else { - allowedHosts := strings.Split(CorsAllowedHosts, ",") - origin := event.Headers["Origin"] - if origin == "" { - origin = event.Headers["origin"] - } - if slices.Contains(allowedHosts, origin) { - headers["Access-Control-Allow-Origin"] = origin - } - } - - var data print2pdf.GetPDFParams - err := json.Unmarshal([]byte(event.Body), &data) - if err != nil { - fmt.Fprintf(os.Stderr, "error decoding JSON: %s\noriginal request body: %s\n", err, event.Body) - - return JsonError("internal server error", 500), nil - } - if !strings.HasSuffix(data.FileName, ".pdf") { - data.FileName += ".pdf" - } - - h, err := print2pdf.NewS3Handler(ctx, BucketName, data.FileName) - if err != nil { - fmt.Fprintf(os.Stderr, "error creating print handler: %s\n", err) - - return JsonError("internal server error", 500), nil - } - - url, err := print2pdf.PrintPDF(ctx, data, h) - if ve, ok := err.(print2pdf.ValidationError); ok { - fmt.Fprintf(os.Stderr, "request validation error: %s\n", ve) - - return JsonError(ve.Error(), 400), nil - } else if err != nil { - fmt.Fprintf(os.Stderr, "error getting PDF: %s\n", err) - - return JsonError("internal server error", 500), nil - } - - body, err := json.Marshal(ResponseData{Url: url}) - if err != nil { - fmt.Fprintf(os.Stderr, "error encoding response to JSON: %s\n", err) +func run() (err error) { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + if err = print2pdf.StartBrowser(ctx); err != nil { + return fmt.Errorf("error starting browser: %s", err) - return JsonError("internal server error", 500), nil } - return events.APIGatewayProxyResponse{ - StatusCode: 200, - Body: string(body), - Headers: headers, - }, nil -} + lambda.StartWithOptions(handler, lambda.WithContext(ctx)) -// Prepare an HTTP error response. -func JsonError(message string, code int) events.APIGatewayProxyResponse { - ct := "application/json" - body, err := json.Marshal(ResponseError{message}) - if err != nil { - fmt.Fprintf(os.Stderr, "error encoding error message to JSON: %s\noriginal error: %s\n", err, message) - body = []byte("internal server error") - code = 500 - ct = "text/plain" - } + <-ctx.Done() + stop() - return events.APIGatewayProxyResponse{ - StatusCode: code, - Body: string(body), - Headers: map[string]string{ - "Content-Type": ct, - "X-Content-Type-Options": "nosniff", - }, - } + return nil } diff --git a/plain/go.mod b/plain/go.mod index a72f877..e71af1b 100644 --- a/plain/go.mod +++ b/plain/go.mod @@ -2,7 +2,15 @@ module github.com/chialab/print2pdf-go/plain go 1.22.5 -require github.com/chialab/print2pdf-go/print2pdf v0.3.1 +require ( + github.com/chialab/print2pdf-go/print2pdf v0.3.1 + github.com/prometheus/client_golang v1.20.3 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/exporters/prometheus v0.52.0 + go.opentelemetry.io/otel/sdk v1.30.0 + go.opentelemetry.io/otel/sdk/metric v1.30.0 +) require ( github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect @@ -24,14 +32,27 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect github.com/aws/smithy-go v1.20.4 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect github.com/chromedp/chromedp v0.10.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/sys v0.25.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/plain/go.sum b/plain/go.sum index 5f4ae18..99a54bc 100644 --- a/plain/go.sum +++ b/plain/go.sum @@ -36,6 +36,10 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8 github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chialab/print2pdf-go/print2pdf v0.3.1 h1:EdTyajMHwaKq7eFD1iF87L0yY3kMgDsmyrHXmyL4e4A= github.com/chialab/print2pdf-go/print2pdf v0.3.1/go.mod h1:zL/TNEhmrcrqJ7ZvDJX+cvWPAah1FyEbNAZ6bVACd4E= github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= @@ -45,23 +49,70 @@ github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKa github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/exporters/prometheus v0.52.0 h1:kmU3H0b9ufFSi8IQCcxack+sWUblKkFbqWYs6YiACGQ= +go.opentelemetry.io/otel/exporters/prometheus v0.52.0/go.mod h1:+wsAp2+JhuGXX7YRkjlkx6hyWY3ogFPfNA4x3nyiAh0= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= +go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plain/handlers.go b/plain/handlers.go new file mode 100644 index 0000000..a292ec1 --- /dev/null +++ b/plain/handlers.go @@ -0,0 +1,205 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "slices" + "strings" + + "github.com/chialab/print2pdf-go/print2pdf" +) + +// Handle requests to "/status" endpoint. +func statusHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + + return + } + + if !print2pdf.Running() { + w.WriteHeader(http.StatusServiceUnavailable) + + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// Handle requests to "/v1/print" endpoint. +func printV1Handler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "OPTIONS": + handlePrintOptions(w, r) + + case "POST": + if BucketName == "" { + fmt.Fprintln(os.Stderr, "missing required environment variable BUCKET") + jsonError(w, "internal server error", http.StatusInternalServerError) + + return + } + + handlePrintV1Post(w, r) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// Handle requests to "/v2/print" endpoint. +func printV2Handler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "OPTIONS": + handlePrintOptions(w, r) + + case "POST": + handlePrintV2Post(w, r) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// Handle OPTIONS requests. +func handlePrintOptions(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Methods", "OPTIONS,POST") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + writeCorsOriginHeader(w, r.Header.Get("Origin")) + w.WriteHeader(http.StatusOK) +} + +// Handle POST requests to "/v1/print" endpoint. +func handlePrintV1Post(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + writeCorsOriginHeader(w, r.Header.Get("Origin")) + data, err := readRequest(r) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + jsonError(w, "internal server error", http.StatusInternalServerError) + + return + } + + h, err := print2pdf.NewS3Handler(r.Context(), BucketName, data.FileName) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating print handler: %s\n", err) + jsonError(w, "internal server error", http.StatusInternalServerError) + + return + } + + res, err := print2pdf.PrintPDF(r.Context(), data, h) + if ve, ok := err.(print2pdf.ValidationError); ok { + fmt.Fprintf(os.Stderr, "request validation error: %s\n", ve) + jsonError(w, ve.Error(), http.StatusBadRequest) + + return + } else if errors.Is(r.Context().Err(), context.Canceled) { + fmt.Println("connection closed or request canceled") + + return + } else if err != nil { + fmt.Fprintf(os.Stderr, "error getting PDF: %s\n", err) + jsonError(w, "internal server error", http.StatusInternalServerError) + + return + } + + body, err := json.Marshal(ResponseData{Url: res}) + if err != nil { + fmt.Fprintf(os.Stderr, "error encoding response to JSON: %s\n", err) + jsonError(w, "internal server error", http.StatusInternalServerError) + + return + } + + _, err = w.Write(body) + if err != nil { + fmt.Fprintf(os.Stderr, "error writing response: %s\n", err) + } +} + +// Handle POST requests to "/v2/print" endpoint. +func handlePrintV2Post(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/pdf") + writeCorsOriginHeader(w, r.Header.Get("Origin")) + data, err := readRequest(r) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + jsonError(w, "internal server error", http.StatusInternalServerError) + + return + } + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", data.FileName)) + h := print2pdf.NewStreamHandler(w) + _, err = print2pdf.PrintPDF(r.Context(), data, h) + if ve, ok := err.(print2pdf.ValidationError); ok { + fmt.Fprintf(os.Stderr, "request validation error: %s\n", ve) + jsonError(w, ve.Error(), http.StatusBadRequest) + } else if errors.Is(r.Context().Err(), context.Canceled) { + fmt.Println("connection closed or request canceled") + } else if err != nil { + fmt.Fprintf(os.Stderr, "error getting PDF: %s\n", err) + jsonError(w, "internal server error", http.StatusInternalServerError) + } +} + +// Read request parameters in structure. +func readRequest(r *http.Request) (print2pdf.GetPDFParams, error) { + body, err := io.ReadAll(r.Body) + if err != nil { + return print2pdf.GetPDFParams{}, fmt.Errorf("error reading request data: %s", err) + } + + var data print2pdf.GetPDFParams + err = json.Unmarshal(body, &data) + if err != nil { + return print2pdf.GetPDFParams{}, fmt.Errorf("error decoding JSON: %s\noriginal request body: %s", err, string(body)) + } + + if !strings.HasSuffix(data.FileName, ".pdf") { + data.FileName += ".pdf" + } + + return data, nil +} + +// Write the "Access-Control-Allow-Origin" header. +func writeCorsOriginHeader(w http.ResponseWriter, origin string) { + if CorsAllowedHosts == "" || CorsAllowedHosts == "*" { + w.Header().Set("Access-Control-Allow-Origin", "*") + } else { + allowedHosts := strings.Split(CorsAllowedHosts, ",") + if slices.Contains(allowedHosts, origin) { + w.Header().Set("Access-Control-Allow-Origin", origin) + } + } +} + +// jsonError replies to the request with the specified error message and HTTP code. +// It does not otherwise end the request; the caller should ensure no further +// writes are done to w. +func jsonError(w http.ResponseWriter, message string, code int) { + w.Header().Set("Content-Type", "application/json") + body, err := json.Marshal(ResponseError{message}) + if err != nil { + fmt.Fprintf(os.Stderr, "error encoding error message to JSON: %s\noriginal error: %s\n", err, message) + body = []byte("internal server error") + code = http.StatusInternalServerError + w.Header().Set("Content-Type", "text/plain") + } + + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + _, err = w.Write(body) + if err != nil { + fmt.Fprintf(os.Stderr, "error writing error response: %s\noriginal response: %s\n", err, string(body)) + } +} diff --git a/plain/main.go b/plain/main.go index c5c92a0..0798c7f 100644 --- a/plain/main.go +++ b/plain/main.go @@ -2,20 +2,24 @@ package main import ( "context" - "encoding/json" "errors" "fmt" - "io" "net" "net/http" "os" "os/signal" "slices" - "strings" "syscall" "time" "github.com/chialab/print2pdf-go/print2pdf" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) type ResponseData struct { @@ -38,7 +42,10 @@ var Port = os.Getenv("PORT") // Comma-separated list of allowed hosts for CORS requests. Defaults to "*", meaning all hosts. var CorsAllowedHosts = os.Getenv("CORS_ALLOWED_HOSTS") -// Init function set default values to environment variables. +// Function to shutdown OpenTelemetry. +var otelShutdown func(context.Context) error + +// Init function set default values to environment variables and initialized OpenTelemetry SDK. func init() { if len(os.Args) > 1 && slices.Contains([]string{"-v", "--version"}, os.Args[1]) { fmt.Printf("Version: %s\n", Version) @@ -47,25 +54,37 @@ func init() { if Port == "" { Port = "3000" } + + var err error + otelShutdown, err = setupOTelSDK() + if err != nil { + fmt.Fprintf(os.Stderr, "error initializing OpenTelemetry: %s\n", err) + os.Exit(1) + } } func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - if err := print2pdf.StartBrowser(ctx); err != nil { - fmt.Fprintf(os.Stderr, "error starting browser: %s\n", err) + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(1) } +} - http.HandleFunc("/status", statusHandler) - http.HandleFunc("/v1/print", printV1Handler) - http.HandleFunc("/v2/print", printV2Handler) +func run() (err error) { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + defer func() { + err = errors.Join(err, otelShutdown(context.Background())) + }() + if err = print2pdf.StartBrowser(ctx); err != nil { + return fmt.Errorf("error starting browser: %s", err) + } srv := &http.Server{ Addr: ":" + Port, BaseContext: func(_ net.Listener) context.Context { return ctx }, ReadTimeout: 10 * time.Second, - Handler: http.DefaultServeMux, + Handler: newHTTPHandler(), } srvErr := make(chan error, 1) go func() { @@ -73,207 +92,61 @@ func main() { srvErr <- srv.ListenAndServe() }() + // Wait for a server error or interrupt signal. select { - case err := <-srvErr: - fmt.Fprintf(os.Stderr, "error starting server: %s\n", err) - os.Exit(1) + case err = <-srvErr: + return fmt.Errorf("error starting server: %s", err) case <-ctx.Done(): stop() } - err := srv.Shutdown(context.Background()) - if err != nil { - fmt.Fprintf(os.Stderr, "error closing server: %s\n", err) - os.Exit(1) - } -} - -// Handle requests to "/status" endpoint. -func statusHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - - return - } - - if !print2pdf.Running() { - w.WriteHeader(http.StatusServiceUnavailable) - - return + if err = srv.Shutdown(context.Background()); err != nil { + return fmt.Errorf("error closing server: %s", err) } - w.WriteHeader(http.StatusNoContent) -} - -// Handle requests to "/v1/print" endpoint. -func printV1Handler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "OPTIONS": - handlePrintOptions(w, r) - - case "POST": - if BucketName == "" { - fmt.Fprintln(os.Stderr, "missing required environment variable BUCKET") - JsonError(w, "internal server error", http.StatusInternalServerError) - - return - } - - handlePrintV1Post(w, r) - - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } + return nil } -// Handle requests to "/v2/print" endpoint. -func printV2Handler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "OPTIONS": - handlePrintOptions(w, r) - - case "POST": - handlePrintV2Post(w, r) - - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +// Create an HTTP handler instrumented by OpenTelemetry. +func newHTTPHandler() http.Handler { + mux := http.NewServeMux() + handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) { + // Configure the "http.route" for the HTTP instrumentation. + handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc)) + mux.Handle(pattern, handler) } -} + handleFunc("/status", statusHandler) + handleFunc("/v1/print", printV1Handler) + handleFunc("/v2/print", printV2Handler) + mux.Handle("/metrics", otelhttp.WithRouteTag("/metrics", promhttp.Handler())) -// Handle OPTIONS requests. -func handlePrintOptions(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "OPTIONS,POST") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - writeCorsOriginHeader(w, r.Header.Get("Origin")) - w.WriteHeader(http.StatusOK) + return otelhttp.NewHandler(mux, "/") } -// Handle POST requests to "/v1/print" endpoint. -func handlePrintV1Post(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - writeCorsOriginHeader(w, r.Header.Get("Origin")) - data, err := readRequest(r) +// Setup OpenTelemetry SDK. +func setupOTelSDK() (func(context.Context) error, error) { + res, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("print2pdf"), + semconv.ServiceVersion(Version), + ), + ) if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - JsonError(w, "internal server error", http.StatusInternalServerError) - - return + return nil, err } - h, err := print2pdf.NewS3Handler(r.Context(), BucketName, data.FileName) + metricExporter, err := prometheus.New() if err != nil { - fmt.Fprintf(os.Stderr, "error creating print handler: %s\n", err) - JsonError(w, "internal server error", http.StatusInternalServerError) - - return + return nil, err } - res, err := print2pdf.PrintPDF(r.Context(), data, h) - if ve, ok := err.(print2pdf.ValidationError); ok { - fmt.Fprintf(os.Stderr, "request validation error: %s\n", ve) - JsonError(w, ve.Error(), http.StatusBadRequest) - - return - } else if errors.Is(r.Context().Err(), context.Canceled) { - fmt.Println("connection closed or request canceled") - - return - } else if err != nil { - fmt.Fprintf(os.Stderr, "error getting PDF: %s\n", err) - JsonError(w, "internal server error", http.StatusInternalServerError) + meterProvider := metric.NewMeterProvider( + metric.WithResource(res), + metric.WithReader(metricExporter), + ) + otel.SetMeterProvider(meterProvider) - return - } - - body, err := json.Marshal(ResponseData{Url: res}) - if err != nil { - fmt.Fprintf(os.Stderr, "error encoding response to JSON: %s\n", err) - JsonError(w, "internal server error", http.StatusInternalServerError) - - return - } - - _, err = w.Write(body) - if err != nil { - fmt.Fprintf(os.Stderr, "error writing response: %s\n", err) - } -} - -// Handle POST requests to "/v2/print" endpoint. -func handlePrintV2Post(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/pdf") - writeCorsOriginHeader(w, r.Header.Get("Origin")) - data, err := readRequest(r) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - JsonError(w, "internal server error", http.StatusInternalServerError) - - return - } - - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", data.FileName)) - h := print2pdf.NewStreamHandler(w) - _, err = print2pdf.PrintPDF(r.Context(), data, h) - if ve, ok := err.(print2pdf.ValidationError); ok { - fmt.Fprintf(os.Stderr, "request validation error: %s\n", ve) - JsonError(w, ve.Error(), http.StatusBadRequest) - } else if errors.Is(r.Context().Err(), context.Canceled) { - fmt.Println("connection closed or request canceled") - } else if err != nil { - fmt.Fprintf(os.Stderr, "error getting PDF: %s\n", err) - JsonError(w, "internal server error", http.StatusInternalServerError) - } -} - -// Read request parameters in structure. -func readRequest(r *http.Request) (print2pdf.GetPDFParams, error) { - body, err := io.ReadAll(r.Body) - if err != nil { - return print2pdf.GetPDFParams{}, fmt.Errorf("error reading request data: %s", err) - } - - var data print2pdf.GetPDFParams - err = json.Unmarshal(body, &data) - if err != nil { - return print2pdf.GetPDFParams{}, fmt.Errorf("error decoding JSON: %s\noriginal request body: %s", err, string(body)) - } - - if !strings.HasSuffix(data.FileName, ".pdf") { - data.FileName += ".pdf" - } - - return data, nil -} - -// Write the "Access-Control-Allow-Origin" header. -func writeCorsOriginHeader(w http.ResponseWriter, origin string) { - if CorsAllowedHosts == "" || CorsAllowedHosts == "*" { - w.Header().Set("Access-Control-Allow-Origin", "*") - } else { - allowedHosts := strings.Split(CorsAllowedHosts, ",") - if slices.Contains(allowedHosts, origin) { - w.Header().Set("Access-Control-Allow-Origin", origin) - } - } -} - -// JsonError replies to the request with the specified error message and HTTP code. -// It does not otherwise end the request; the caller should ensure no further -// writes are done to w. -func JsonError(w http.ResponseWriter, message string, code int) { - w.Header().Set("Content-Type", "application/json") - body, err := json.Marshal(ResponseError{message}) - if err != nil { - fmt.Fprintf(os.Stderr, "error encoding error message to JSON: %s\noriginal error: %s\n", err, message) - body = []byte("internal server error") - code = http.StatusInternalServerError - w.Header().Set("Content-Type", "text/plain") - } - - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(code) - _, err = w.Write(body) - if err != nil { - fmt.Fprintf(os.Stderr, "error writing error response: %s\noriginal response: %s\n", err, string(body)) - } + return meterProvider.Shutdown, nil } diff --git a/print2pdf/print2pdf.go b/print2pdf/print2pdf.go index 322920e..0bec3a8 100644 --- a/print2pdf/print2pdf.go +++ b/print2pdf/print2pdf.go @@ -150,20 +150,15 @@ var ChromiumPath = os.Getenv("CHROMIUM_PATH") // Reference to browser context, initialized in init function of this package. var browserCtx context.Context -// Init function checks for required environment variables. -func init() { - if ChromiumPath == "" { - fmt.Fprintln(os.Stderr, "missing required environment variable CHROMIUM_PATH") - os.Exit(1) - } -} - // Allocate a browser to be reused by multiple invocations, to reduce startup time. Cancelling the context will close the browser. // This function must be called before starting to print PDFs. func StartBrowser(ctx context.Context) error { if Running() { return nil } + if ChromiumPath == "" { + return fmt.Errorf("missing required environment variable CHROMIUM_PATH") + } defer Elapsed("Browser startup")() opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(ChromiumPath))