Skip to content

Commit

Permalink
feat: export metrics for Prometheus with OpenTelemetry instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
le0m committed Sep 13, 2024
1 parent 47509df commit ef46e79
Show file tree
Hide file tree
Showing 11 changed files with 488 additions and 292 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -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=
1 change: 1 addition & 0 deletions lambda/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
4 changes: 2 additions & 2 deletions lambda/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
93 changes: 93 additions & 0 deletions lambda/handlers.go
Original file line number Diff line number Diff line change
@@ -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",
},
}
}
96 changes: 11 additions & 85 deletions lambda/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
23 changes: 22 additions & 1 deletion plain/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
Loading

0 comments on commit ef46e79

Please sign in to comment.