diff --git a/.dockerignore b/.dockerignore index f4269d3..4f836b9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,9 @@ * +!.git !.promu.yml -!passenger_exporter.go -!passenger_exporter_test.go +!go.mod +!go.sum +!main.go !Makefile -!vendor/ !VERSION diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..945c917 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,47 @@ +name: tests + +on: + pull_request: + branches: + - master + push: + branches: + - master + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.x + cache: true + - name: Run Linter + uses: golangci/golangci-lint-action@v3 + with: + version: v1.51.2 + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.x + cache: true + - name: Build + run: go build -v ./... + - name: Run Tests + run: go test -json ./... > test-results.json + - name: Annotate Tests + if: always() + uses: guyarb/golang-test-annotations@v0.6.0 + with: + test-results: test-results.json diff --git a/.gitignore b/.gitignore index 9e38b36..9c33d11 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -passenger-exporter + +bin/ +vendor/ diff --git a/.promu.yml b/.promu.yml index fde61e4..1a4c5bb 100644 --- a/.promu.yml +++ b/.promu.yml @@ -1,18 +1,14 @@ go: - version: 1.9.4 + version: 1.20.1 repository: path: github.com/Intellection/passenger-exporter build: binaries: - name: passenger-exporter - flags: -a -tags -installsuffix + flags: -a -tags -netgo ldflags: | - -X {{repoPath}}/vendor/github.com/prometheus/common/version.Version={{.Version}} - -X {{repoPath}}/vendor/github.com/prometheus/common/version.Revision={{.Revision}} - -X {{repoPath}}/vendor/github.com/prometheus/common/version.Branch={{.Branch}} - -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildUser={{user}}@{{host}} - -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} -tarball: - files: - - LICENSE - - NOTICE + -X github.com/prometheus/common/version.Version={{.Version}} + -X github.com/prometheus/common/version.Revision={{.Revision}} + -X github.com/prometheus/common/version.Branch={{.Branch}} + -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} + -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3092bfa --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug exporter", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "env": { "PASSENGER_INSTANCE_REGISTRY_DIR": "/tmp/passenger"} + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 80836c2..fdb0607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 1.0.0 + +### Breaking Changes +* Rename `app_count` metric to `app_group_count`. +* Update `proc_start_time_seconds` metric to correctly be in unix seconds. +* Update default value for `-passenger.command.timeout-seconds` flag to `5s`. +* Remove process metrics collector. +* Remomve `/` endpoint showing link to metrics path. + +### Bug Fixes +* Prevent index out of range panics when the number of passenger processes surges past the max pool size temporarily when replacing an existing process. + +### Improvements +* Upgrade to Go `v1.20.1`. +* Switch Go modules. +* Upgrade Go dependencies. +* Upgrade bundled Passenger to `v6.0.17`. +* Switch container image from Alpine Linux to Debian Bullseye. +* Use builder pattern to build binary and copy it into runner image. +* Run container as `exporter` user instead of `nobody`. +* Add new fields parsed from passenger status command. +* Use expected types in structs instead of parsing afterwards. +* Configure `promu` to use `netgo` instead of `installsuffix`. +* Switch to `sirupsen/logrus` logger as `prometheus/common` no longer includes it. +* Add launch configuration for debugging in Visual Studio Code. +* Add GitHub Actions workflow for testing and linting. +* Simplify Makefile for single command builds. + ## 0.7.1 ### Bug Fixes diff --git a/CODEOWNERS b/CODEOWNERS index 0db5bca..9cd5e24 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @Intellection/devops +* @Intellection/sre diff --git a/Dockerfile b/Dockerfile index e416b6b..29cd7ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,54 @@ -FROM ruby:2.4.3-alpine3.7 +ARG GOLANG_VERSION="1.20.1" +ARG DEBIAN_VERSION="bullseye-20230227-slim" -ARG GOLANG_VERSION="1.9.4-r0" -ARG BUILD_DEPS="go=$GOLANG_VERSION ruby-dev linux-headers curl curl-dev pcre-dev libexecinfo-dev@edge-main" -ARG RUNTIME_DEPS="tini build-base pcre git libexecinfo@edge-main" +ARG BUILDER_IMAGE="golang:${GOLANG_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" -RUN echo '@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main/' >> /etc/apk/repositories && \ - apk update && \ - apk upgrade && \ - apk add $BUILD_DEPS && \ - apk add $RUNTIME_DEPS && \ - mkdir -p /opt +FROM ${BUILDER_IMAGE} AS builder -# Passenger -ENV PASSENGER_VERSION="5.1.12" \ - PATH="/opt/passenger/bin:$PATH" -RUN curl -L "https://s3.amazonaws.com/phusion-passenger/releases/passenger-$PASSENGER_VERSION.tar.gz" | tar -xzf - -C /opt && \ - mv /opt/passenger-$PASSENGER_VERSION /opt/passenger && \ - passenger-config validate-install --auto && \ - export EXTRA_PRE_CFLAGS='-O' EXTRA_PRE_CXXFLAGS='-O' EXTRA_LDFLAGS='-lexecinfo' && \ - passenger-config compile-agent --optimize && \ - passenger-config install-standalone-runtime && \ - passenger-config build-native-support +WORKDIR /opt/app + +# Install build tools +RUN go install github.com/prometheus/promu@v0.14.0 + +# Add source files and build +ADD . ./ +RUN make -# Go configuration -ENV GOROOT="/usr/lib/go" \ - GOPATH="/go" -ENV PATH="$GOPATH/bin:$GOROOT/bin:$PATH" +FROM ${RUNNER_IMAGE} -# Go dependencies -RUN go get github.com/prometheus/promu +RUN apt-get update -y && \ + apt-get install -y \ + bash \ + ca-certificates \ + gnupg && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* -# Configure source path -ARG SOURCE_PATH="/go/src/github.com/Intellection/passenger-exporter" -RUN mkdir -p ${SOURCE_PATH} +ARG APP_USER="exporter" +ENV APP_HOME="/opt/app" -# Add source files -ADD . ${SOURCE_PATH}/ -WORKDIR ${SOURCE_PATH} +WORKDIR ${APP_HOME} -# Build exporter -RUN promu build && \ - mv ${SOURCE_PATH}/passenger-exporter /usr/local/bin/passenger-exporter && \ - rm -rf ${SOURCE_PATH}/* +# Create user +RUN groupadd -g 9999 ${APP_USER} && \ + useradd --system --create-home -u 9999 -g 9999 ${APP_USER} + +# Passenger +ARG PASSENGER_VERSION="6.0.17" +ARG PASSENGER_PKG="1:${PASSENGER_VERSION}-1~bullseye1" +RUN apt-key adv --no-tty --keyserver hkps://keyserver.ubuntu.com --recv-keys 561F9B9CAC40B2F7 && \ + echo 'deb https://oss-binaries.phusionpassenger.com/apt/passenger bullseye main' > /etc/apt/sources.list.d/passenger.list && \ + apt-get update -y && \ + apt-get install -y passenger=${PASSENGER_PKG} && \ + passenger-config validate-install --auto && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* -# Cleanup -RUN apk del $BUILD_PACKAGES && \ - rm -rf /var/cache/apk/* +# Copy files from builder +COPY --from=builder --chown=${APP_USER}:${APP_USER} ${APP_HOME}/bin/ ./bin/ -USER nobody:nobody +# Run as user +USER ${APP_USER}:${APP_USER} -ENTRYPOINT ["tini", "--", "passenger-exporter"] -CMD ["/bin/sh"] +ENTRYPOINT ["./bin/passenger-exporter"] diff --git a/Makefile b/Makefile index 1dcac10..a3c04d5 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,24 @@ -VERSION := $(shell cat VERSION) -BIN := passenger-exporter -CONTAINER := passenger-exporter -GOOS ?= linux -GOARCH ?= amd64 +BIN_DIR := ./bin -GOFLAGS := -ldflags "-X main.Version=$(VERSION)" -a -installsuffix cgo -TAR := $(BIN)-$(VERSION)-$(GOOS)-$(GOARCH).tar.gz -DST ?= http://ent.int.s-cloud.net/iss/$(BIN) +.PHONY: all +all: dependencies build -PREFIX ?= $(shell pwd) +.PHONY: build +build: + promu build --prefix=$(BIN_DIR) -default: $(BIN) +.PHONY: test +test: + go test -v ./... -$(BIN): - CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) promu build --prefix $(PREFIX) +.PHONY: lint +lint: + golangci-lint run -release: $(TAR) - curl -XPOST --data-binary @$< $(DST)/$< +.PHONY: dependencies +dependencies: + go mod vendor -build-docker: $(BIN) - docker build -t $(CONTAINER) . - -$(TAR): $(BIN) - tar czf $@ $< +.PHONY: clean +clean: + rm -rf ${BIN_DIR} diff --git a/README.md b/README.md index db85e52..f25c3b2 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,60 @@ # Passenger Exporter -Prometheus exporter for [Phusion Passenger](https://www.phusionpassenger.com) metrics. +Passenger exporter is a [Prometheus] metrics exporter for [Phusion Passenger] 6.0. -## Flags +## Usage +### Running locally +Start Passenger with a known [instance registry]: +```console +$ PASSENGER_INSTANCE_REGISTRY_DIR=/tmp/passenger passenger start ``` - -log.format value - If set use a syslog logger or JSON logging. - Example: logger:syslog?appname=bob&local=7 or logger:stdout?json=true. - Defaults to stderr. - -log.level value - Only log messages with the given severity or above. - Valid levels: [debug, info, warn, error, fatal]. (default info) - -passenger.command string - Passenger command for querying passenger status. - (default "passenger-status --show=xml") - -passenger.pid-file string - Optional path to a file containing the passenger PID for additional metrics. - -passenger.command.timeout-seconds float - Timeout for passenger.command. (default 0.5 seconds) - -web.listen-address string - Address to listen on for web interface and telemetry. (default ":9149") - -web.telemetry-path string - Path under which to expose metrics. (default "/metrics") + +Run the exporter, specifying the same [instance registry]: +```console +$ PASSENGER_INSTANCE_REGISTRY_DIR=/tmp/passenger ./bin/passenger-exporter ``` +**Note**: You must specify the [`PASSENGER_INSTANCE_REGISTRY_DIR`] environment variable. It is used by the [`passenger-status`] command to query Passenger information. Without it, the exporter cannot retreve metrics from your Passenger instance. -## Running Tests +### Flags -Tests can be run with: -``` -go test . -``` +The following flags are available: -Additionally, the test/scrape_output.txt can be regenerated by passing the -`--golden` flag: -``` -go test -v . --golden -``` +| Flag | Description | Default Value | +|-----------------------------------------|--------------------------------------------------------|----------------------------| +| `-passenger.command string` | Passenger command for querying passenger status. | `"passenger-status --show=xml"` | +| `-passenger.command.timeout-seconds float` | Timeout for passenger.command. | `5` | +| `-web.listen-address string` | Address to listen on for web interface and telemetry. | `":9149"` | +| `-web.telemetry-path string` | Path under which to expose metrics. | `"/metrics"` | + + +## Building + +1. Clone the repository. +2. Install the [`promu`] build tool. +2. Install application dependencies via `make dependencies` (they'll be placed in `./vendor`). +3. Build and install the binary with `make build`. +4. Run the command e.g. `./bin/passenger-exporter -h`. + +## Testing + +1. Install the `golangci-lint`, [see instructions here][golangci-lint-install]. +2. Run linter using `make lint` and test using `make test`. + +The scrape output can be regenerated by passing the `--golden` flag to `go test`. + +## Credits + +We would like to acknowledge and express our gratitude to [@stuartnelson3] for their work on the [upstream version of Passenger Exporter]. + +[`PASSENGER_INSTANCE_REGISTRY_DIR`]: https://www.phusionpassenger.com/library/config/standalone/reference/#--instance-registry-dir-instance_registry_dir +[`passenger-status`]: https://www.phusionpassenger.com/library/admin/standalone/overall_status_report.html +[`promu`]: github.com/prometheus/promu +[golang-quickstart]: https://go.dev/doc/tutorial/getting-started +[golangci-lint-install]: https://golangci-lint.run/usage/install/ +[Phusion Passenger]: https://www.phusionpassenger.com/ +[Prometheus]: https://prometheus.io/ +[instance registry]: https://www.phusionpassenger.com/library/config/standalone/reference/#--instance-registry-dir-instance_registry_dir +[upstream version of Passenger Exporter]: https://github.com/stuartnelson3/passenger_exporter +[@stuartnelson3]: https://github.com/stuartnelson3/ diff --git a/VERSION b/VERSION index 39e898a..3eefcb9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.1 +1.0.0 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..431b160 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/Intellection/passenger-exporter + +go 1.20 + +require ( + github.com/prometheus/client_golang v1.14.0 + github.com/prometheus/common v0.41.0 + github.com/sirupsen/logrus v1.9.0 + golang.org/x/net v0.7.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..62067e4 --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +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.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.41.0 h1:npo01n6vUlRViIj5fgwiK8vlNIh8bnoxqh3gypKsyAw= +github.com/prometheus/common v0.41.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/passenger_exporter.go b/main.go similarity index 62% rename from passenger_exporter.go rename to main.go index 96f629e..cf4a0bb 100644 --- a/passenger_exporter.go +++ b/main.go @@ -6,8 +6,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" - "math" "net/http" "os/exec" "strconv" @@ -17,18 +15,19 @@ import ( "golang.org/x/net/html/charset" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/common/log" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/version" + "github.com/sirupsen/logrus" ) // Info represents the info section of passenger's status. type Info struct { PassengerVersion string `xml:"passenger_version"` - AppCount string `xml:"group_count"` - CurrentProcessCount string `xml:"process_count"` - MaxProcessCount string `xml:"max"` - CapacityUsed string `xml:"capacity_used"` - TopLevelRequestQueueSize string `xml:"get_wait_list_size"` + AppGroupCount int64 `xml:"group_count"` + CurrentProcessCount int64 `xml:"process_count"` + MaxProcessCount int64 `xml:"max"` + CapacityUsed int64 `xml:"capacity_used"` + TopLevelRequestQueueSize int64 `xml:"get_wait_list_size"` SuperGroups []SuperGroup `xml:"supergroups>supergroup"` } @@ -36,8 +35,8 @@ type Info struct { type SuperGroup struct { Name string `xml:"name"` State string `xml:"state"` - RequestQueueSize string `xml:"get_wait_list_size"` - CapacityUsed string `xml:"capacity_used"` + RequestQueueSize int64 `xml:"get_wait_list_size"` + CapacityUsed int64 `xml:"capacity_used"` Group Group `xml:"group"` } @@ -49,19 +48,19 @@ type Group struct { AppType string `xml:"app_type"` Environment string `xml:"environment"` UUID string `xml:"uuid"` - EnabledProcessCount string `xml:"enabled_process_count"` - DisablingProcessCount string `xml:"disabling_process_count"` - DisabledProcessCount string `xml:"disabled_process_count"` - CapacityUsed string `xml:"capacity_used"` - RequestQueueSize string `xml:"get_wait_list_size"` - DisableWaitListSize string `xml:"disable_wait_list_size"` - ProcessesSpawning string `xml:"processes_being_spawned"` + EnabledProcessCount int64 `xml:"enabled_process_count"` + DisablingProcessCount int64 `xml:"disabling_process_count"` + DisabledProcessCount int64 `xml:"disabled_process_count"` + CapacityUsed int64 `xml:"capacity_used"` + RequestQueueSize int64 `xml:"get_wait_list_size"` + DisableWaitListSize int64 `xml:"disable_wait_list_size"` + ProcessesSpawning int64 `xml:"processes_being_spawned"` LifeStatus string `xml:"life_status"` User string `xml:"user"` - UID string `xml:"uid"` + UID int64 `xml:"uid"` Group string `xml:"group"` - GID string `xml:"gid"` - Default string `xml:"default,attr"` + GID int64 `xml:"gid"` + Default bool `xml:"default,attr"` Options Options `xml:"options"` Processes []Process `xml:"processes>process"` } @@ -71,68 +70,68 @@ type Process struct { PID string `xml:"pid"` StickySessionID string `xml:"sticky_session_id"` GUPID string `xml:"gupid"` - Concurrency string `xml:"concurrency"` - Sessions string `xml:"sessions"` - Busyness string `xml:"busyness"` - RequestsProcessed string `xml:"processed"` - SpawnerCreationTime string `xml:"spawner_creation_time"` - SpawnStartTime string `xml:"spawn_start_time"` - SpawnEndTime string `xml:"spawn_end_time"` - LastUsed string `xml:"last_used"` + Concurrency int64 `xml:"concurrency"` + Sessions int64 `xml:"sessions"` + Busyness int64 `xml:"busyness"` + RequestsProcessed int64 `xml:"processed"` + SpawnerCreationTime int64 `xml:"spawner_creation_time"` + SpawnStartTime int64 `xml:"spawn_start_time"` + SpawnEndTime int64 `xml:"spawn_end_time"` + LastUsed int64 `xml:"last_used"` LastUsedDesc string `xml:"last_used_desc"` Uptime string `xml:"uptime"` LifeStatus string `xml:"life_status"` Enabled string `xml:"enabled"` - HasMetrics string `xml:"has_metrics"` - CPU string `xml:"cpu"` - RSS string `xml:"rss"` - PSS string `xml:"pss"` - PrivateDirty string `xml:"private_dirty"` - Swap string `xml:"swap"` - RealMemory string `xml:"real_memory"` - VMSize string `xml:"vmsize"` + HasMetrics bool `xml:"has_metrics"` + CPU int64 `xml:"cpu"` + RSS int64 `xml:"rss"` + PSS int64 `xml:"pss"` + PrivateDirty int64 `xml:"private_dirty"` + Swap int64 `xml:"swap"` + RealMemory int64 `xml:"real_memory"` + VMSize int64 `xml:"vmsize"` ProcessGroupID string `xml:"process_group_id"` Command string `xml:"command"` } // Options represents the options section of passenger's status. type Options struct { - AppRoot string `xml:"app_root"` - AppGroupName string `xml:"app_group_name"` - AppType string `xml:"app_type"` - StartCommand string `xml:"start_command"` - StartupFile string `xml:"startup_file"` - ProcessTitle string `xml:"process_title"` - LogLevel string `xml:"log_level"` - StartTimeout string `xml:"start_timeout"` - Environment string `xml:"environment"` - BaseURI string `xml:"base_uri"` - SpawnMethod string `xml:"spawn_method"` - DefaultUser string `xml:"default_user"` - DefaultGroup string `xml:"default_group"` - IntegrationMode string `xml:"integration_mode"` - RubyBinPath string `xml:"ruby"` - PythonBinPath string `xml:"python"` - NodeJSBinPath string `xml:"nodejs"` - USTRouterAddress string `xml:"ust_router_address"` - USTRouterUsername string `xml:"ust_router_username"` - USTRouterPassword string `xml:"ust_router_password"` - Debugger string `xml:"debugger"` - Analytics string `xml:"analytics"` - APIKey string `xml:"api_key"` - MinProcesses string `xml:"min_processes"` - MaxProcesses string `xml:"max_processes"` - MaxPreloaderIdleTime string `xml:"max_preloader_idle_time"` - MaxOutOfBandWorkInstances string `xml:"max_out_of_band_work_instances"` + AppRoot string `xml:"app_root"` + AppGroupName string `xml:"app_group_name"` + AppType string `xml:"app_type"` + StartCommand string `xml:"start_command"` + StartupFile string `xml:"startup_file"` + ProcessTitle string `xml:"process_title"` + LogLevel int64 `xml:"log_level"` + StartTimeout int64 `xml:"start_timeout"` + Environment string `xml:"environment"` + BaseURI string `xml:"base_uri"` + SpawnMethod string `xml:"spawn_method"` + BindAddress string `xml:"bind_address"` + DefaultUser string `xml:"default_user"` + DefaultGroup string `xml:"default_group"` + RestartDirectory string `xml:"restart_dir"` + IntegrationMode string `xml:"integration_mode"` + RubyBinPath string `xml:"ruby"` + PythonBinPath string `xml:"python"` + NodeJSBinPath string `xml:"nodejs"` + Debugger bool `xml:"debugger"` + APIKey string `xml:"api_key"` + MinProcesses int64 `xml:"min_processes"` + MaxProcesses int64 `xml:"max_processes"` + MaxPreloaderIdleTime int64 `xml:"max_preloader_idle_time"` + MaxOutOfBandWorkInstances int64 `xml:"max_out_of_band_work_instances"` + StickySessionCookieAttributes string `xml:"sticky_sessions_cookie_attributes"` } const ( - namespace = "passenger" - nanosecondsPerSecond = 1000000000 + namespace = "passenger" + microsecondsPerSecond = 1000000 ) var ( processIdentifiers = make(map[string]int) + log = logrus.New() ) // Exporter collects metrics from passenger. @@ -150,7 +149,7 @@ type Exporter struct { topLevelRequestQueue *prometheus.Desc maxProcessCount *prometheus.Desc currentProcessCount *prometheus.Desc - appCount *prometheus.Desc + appGroupCount *prometheus.Desc // App metrics. appRequestQueue *prometheus.Desc @@ -165,11 +164,11 @@ type Exporter struct { // NewExporter returns an initialized exporter. func NewExporter(cmd string, timeout float64) *Exporter { cmdComponents := strings.Split(cmd, " ") - + timeoutDuration := time.Duration(timeout * float64(time.Second)) return &Exporter{ cmd: cmdComponents[0], args: cmdComponents[1:], - timeout: time.Duration(timeout * nanosecondsPerSecond), + timeout: timeoutDuration, up: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "up"), "Current health of passenger.", @@ -200,9 +199,9 @@ func NewExporter(cmd string, timeout float64) *Exporter { nil, nil, ), - appCount: prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "app_count"), - "Number of apps.", + appGroupCount: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "app_group_count"), + "Number of app groups.", nil, nil, ), @@ -246,7 +245,7 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- e.topLevelRequestQueue ch <- e.maxProcessCount ch <- e.currentProcessCount - ch <- e.appCount + ch <- e.appGroupCount ch <- e.appRequestQueue ch <- e.appProcsSpawning ch <- e.requestsProcessed @@ -266,27 +265,24 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, 1) ch <- prometheus.MustNewConstMetric(e.version, prometheus.GaugeValue, 1, info.PassengerVersion) - ch <- prometheus.MustNewConstMetric(e.topLevelRequestQueue, prometheus.GaugeValue, parseFloat(info.TopLevelRequestQueueSize)) - ch <- prometheus.MustNewConstMetric(e.maxProcessCount, prometheus.GaugeValue, parseFloat(info.MaxProcessCount)) - ch <- prometheus.MustNewConstMetric(e.currentProcessCount, prometheus.GaugeValue, parseFloat(info.CurrentProcessCount)) - ch <- prometheus.MustNewConstMetric(e.appCount, prometheus.GaugeValue, parseFloat(info.AppCount)) + ch <- prometheus.MustNewConstMetric(e.topLevelRequestQueue, prometheus.GaugeValue, float64(info.TopLevelRequestQueueSize)) + ch <- prometheus.MustNewConstMetric(e.maxProcessCount, prometheus.GaugeValue, float64(info.MaxProcessCount)) + ch <- prometheus.MustNewConstMetric(e.currentProcessCount, prometheus.GaugeValue, float64(info.CurrentProcessCount)) + ch <- prometheus.MustNewConstMetric(e.appGroupCount, prometheus.GaugeValue, float64(info.AppGroupCount)) for _, sg := range info.SuperGroups { - ch <- prometheus.MustNewConstMetric(e.appRequestQueue, prometheus.GaugeValue, parseFloat(sg.Group.RequestQueueSize), sg.Name) - ch <- prometheus.MustNewConstMetric(e.appProcsSpawning, prometheus.GaugeValue, parseFloat(sg.Group.ProcessesSpawning), sg.Name) + ch <- prometheus.MustNewConstMetric(e.appRequestQueue, prometheus.GaugeValue, float64(sg.Group.RequestQueueSize), sg.Name) + ch <- prometheus.MustNewConstMetric(e.appProcsSpawning, prometheus.GaugeValue, float64(sg.Group.ProcessesSpawning), sg.Name) // Update process identifiers map. - processIdentifiers = updateProcesses(processIdentifiers, sg.Group.Processes, parseInt(info.MaxProcessCount)) + processIdentifiers = updateProcesses(processIdentifiers, sg.Group.Processes, int(info.MaxProcessCount)) for _, proc := range sg.Group.Processes { if bucketID, ok := processIdentifiers[proc.PID]; ok { - ch <- prometheus.MustNewConstMetric(e.procMemory, prometheus.GaugeValue, parseFloat(proc.RealMemory), sg.Name, strconv.Itoa(bucketID)) - ch <- prometheus.MustNewConstMetric(e.requestsProcessed, prometheus.CounterValue, parseFloat(proc.RequestsProcessed), sg.Name, strconv.Itoa(bucketID)) - - if startTime, err := strconv.Atoi(proc.SpawnStartTime); err == nil { - ch <- prometheus.MustNewConstMetric(e.procStartTime, prometheus.GaugeValue, float64(startTime/nanosecondsPerSecond), - sg.Name, strconv.Itoa(bucketID), - ) - } + ch <- prometheus.MustNewConstMetric(e.procMemory, prometheus.GaugeValue, float64(proc.RealMemory), sg.Name, strconv.Itoa(bucketID)) + ch <- prometheus.MustNewConstMetric(e.requestsProcessed, prometheus.CounterValue, float64(proc.RequestsProcessed), sg.Name, strconv.Itoa(bucketID)) + ch <- prometheus.MustNewConstMetric(e.procStartTime, prometheus.GaugeValue, float64(proc.SpawnStartTime/microsecondsPerSecond), + sg.Name, strconv.Itoa(bucketID), + ) } } } @@ -336,24 +332,6 @@ func parseOutput(r io.Reader) (*Info, error) { return &info, nil } -func parseFloat(val string) float64 { - v, err := strconv.ParseFloat(val, 64) - if err != nil { - log.Errorf("failed to parse %s: %v", val, err) - v = math.NaN() - } - return v -} - -func parseInt(val string) int { - v, err := strconv.Atoi(val) - if err != nil { - log.Errorf("failed to parse %s: %v", val, err) - v = 0 - } - return v -} - // updateProcesses updates the global map from process id:exporter id. Process // TTLs cause new processes to be created on a user-defined cycle. When a new // process replaces an old process, the new process's statistics will be @@ -367,11 +345,15 @@ func parseInt(val string) int { // pid:id pair in the map. func updateProcesses(old map[string]int, processes []Process, maxProcesses int) map[string]int { var ( - updated = make(map[string]int) + updated = make(map[string]int, maxProcesses) found = make([]string, maxProcesses) missing []string ) + if len(processes) > maxProcesses { + processes = processes[:maxProcesses] + } + for _, p := range processes { if id, ok := old[p.PID]; ok { found[id] = p.PID @@ -419,45 +401,24 @@ func updateProcesses(old map[string]int, processes []Process, maxProcesses int) func main() { var ( cmd = flag.String("passenger.command", "passenger-status --show=xml", "Passenger command for querying passenger status.") - timeout = flag.Float64("passenger.command.timeout-seconds", 0.5, "Timeout in seconds for passenger.command.") - pidFile = flag.String("passenger.pid-file", "", "Optional path to a file containing the passenger PID for additional metrics.") + timeout = flag.Float64("passenger.command.timeout-seconds", 5, "Timeout in seconds for passenger.command.") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") listenAddress = flag.String("web.listen-address", ":9149", "Address to listen on for web interface and telemetry.") ) flag.Parse() - if *pidFile != "" { - prometheus.MustRegister(prometheus.NewProcessCollectorPIDFn( - func() (int, error) { - content, err := ioutil.ReadFile(*pidFile) - if err != nil { - return 0, fmt.Errorf("error reading pidfile %q: %s", *pidFile, err) - } - value, err := strconv.Atoi(strings.TrimSpace(string(content))) - if err != nil { - return 0, fmt.Errorf("error parsing pidfile %q: %s", *pidFile, err) - } - return value, nil - }, - namespace), - ) - } + // Create a new registry. + reg := prometheus.NewRegistry() - prometheus.MustRegister(NewExporter(*cmd, *timeout)) + // Add Go module build info. + reg.MustRegister(NewExporter(*cmd, *timeout)) - http.Handle(*metricsPath, prometheus.Handler()) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(` -
block. - if d != "" && d[0] == '\r' { - d = d[1:] - } - if d != "" && d[0] == '\n' { - d = d[1:] - } - } - } - d = strings.Replace(d, "\x00", "", -1) - if d == "" { - return true - } - p.reconstructActiveFormattingElements() - p.addText(d) - if p.framesetOK && strings.TrimLeft(d, whitespace) != "" { - // There were non-whitespace characters inserted. - p.framesetOK = false - } - case StartTagToken: - switch p.tok.DataAtom { - case a.Html: - copyAttributes(p.oe[0], p.tok) - case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title: - return inHeadIM(p) - case a.Body: - if len(p.oe) >= 2 { - body := p.oe[1] - if body.Type == ElementNode && body.DataAtom == a.Body { - p.framesetOK = false - copyAttributes(body, p.tok) - } - } - case a.Frameset: - if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body { - // Ignore the token. - return true - } - body := p.oe[1] - if body.Parent != nil { - body.Parent.RemoveChild(body) - } - p.oe = p.oe[:1] - p.addElement() - p.im = inFramesetIM - return true - case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul: - p.popUntil(buttonScope, a.P) - p.addElement() - case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: - p.popUntil(buttonScope, a.P) - switch n := p.top(); n.DataAtom { - case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: - p.oe.pop() - } - p.addElement() - case a.Pre, a.Listing: - p.popUntil(buttonScope, a.P) - p.addElement() - // The newline, if any, will be dealt with by the TextToken case. - p.framesetOK = false - case a.Form: - if p.form == nil { - p.popUntil(buttonScope, a.P) - p.addElement() - p.form = p.top() - } - case a.Li: - p.framesetOK = false - for i := len(p.oe) - 1; i >= 0; i-- { - node := p.oe[i] - switch node.DataAtom { - case a.Li: - p.oe = p.oe[:i] - case a.Address, a.Div, a.P: - continue - default: - if !isSpecialElement(node) { - continue - } - } - break - } - p.popUntil(buttonScope, a.P) - p.addElement() - case a.Dd, a.Dt: - p.framesetOK = false - for i := len(p.oe) - 1; i >= 0; i-- { - node := p.oe[i] - switch node.DataAtom { - case a.Dd, a.Dt: - p.oe = p.oe[:i] - case a.Address, a.Div, a.P: - continue - default: - if !isSpecialElement(node) { - continue - } - } - break - } - p.popUntil(buttonScope, a.P) - p.addElement() - case a.Plaintext: - p.popUntil(buttonScope, a.P) - p.addElement() - case a.Button: - p.popUntil(defaultScope, a.Button) - p.reconstructActiveFormattingElements() - p.addElement() - p.framesetOK = false - case a.A: - for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- { - if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A { - p.inBodyEndTagFormatting(a.A) - p.oe.remove(n) - p.afe.remove(n) - break - } - } - p.reconstructActiveFormattingElements() - p.addFormattingElement() - case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U: - p.reconstructActiveFormattingElements() - p.addFormattingElement() - case a.Nobr: - p.reconstructActiveFormattingElements() - if p.elementInScope(defaultScope, a.Nobr) { - p.inBodyEndTagFormatting(a.Nobr) - p.reconstructActiveFormattingElements() - } - p.addFormattingElement() - case a.Applet, a.Marquee, a.Object: - p.reconstructActiveFormattingElements() - p.addElement() - p.afe = append(p.afe, &scopeMarker) - p.framesetOK = false - case a.Table: - if !p.quirks { - p.popUntil(buttonScope, a.P) - } - p.addElement() - p.framesetOK = false - p.im = inTableIM - return true - case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr: - p.reconstructActiveFormattingElements() - p.addElement() - p.oe.pop() - p.acknowledgeSelfClosingTag() - if p.tok.DataAtom == a.Input { - for _, t := range p.tok.Attr { - if t.Key == "type" { - if strings.ToLower(t.Val) == "hidden" { - // Skip setting framesetOK = false - return true - } - } - } - } - p.framesetOK = false - case a.Param, a.Source, a.Track: - p.addElement() - p.oe.pop() - p.acknowledgeSelfClosingTag() - case a.Hr: - p.popUntil(buttonScope, a.P) - p.addElement() - p.oe.pop() - p.acknowledgeSelfClosingTag() - p.framesetOK = false - case a.Image: - p.tok.DataAtom = a.Img - p.tok.Data = a.Img.String() - return false - case a.Isindex: - if p.form != nil { - // Ignore the token. - return true - } - action := "" - prompt := "This is a searchable index. Enter search keywords: " - attr := []Attribute{{Key: "name", Val: "isindex"}} - for _, t := range p.tok.Attr { - switch t.Key { - case "action": - action = t.Val - case "name": - // Ignore the attribute. - case "prompt": - prompt = t.Val - default: - attr = append(attr, t) - } - } - p.acknowledgeSelfClosingTag() - p.popUntil(buttonScope, a.P) - p.parseImpliedToken(StartTagToken, a.Form, a.Form.String()) - if action != "" { - p.form.Attr = []Attribute{{Key: "action", Val: action}} - } - p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String()) - p.parseImpliedToken(StartTagToken, a.Label, a.Label.String()) - p.addText(prompt) - p.addChild(&Node{ - Type: ElementNode, - DataAtom: a.Input, - Data: a.Input.String(), - Attr: attr, - }) - p.oe.pop() - p.parseImpliedToken(EndTagToken, a.Label, a.Label.String()) - p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String()) - p.parseImpliedToken(EndTagToken, a.Form, a.Form.String()) - case a.Textarea: - p.addElement() - p.setOriginalIM() - p.framesetOK = false - p.im = textIM - case a.Xmp: - p.popUntil(buttonScope, a.P) - p.reconstructActiveFormattingElements() - p.framesetOK = false - p.addElement() - p.setOriginalIM() - p.im = textIM - case a.Iframe: - p.framesetOK = false - p.addElement() - p.setOriginalIM() - p.im = textIM - case a.Noembed, a.Noscript: - p.addElement() - p.setOriginalIM() - p.im = textIM - case a.Select: - p.reconstructActiveFormattingElements() - p.addElement() - p.framesetOK = false - p.im = inSelectIM - return true - case a.Optgroup, a.Option: - if p.top().DataAtom == a.Option { - p.oe.pop() - } - p.reconstructActiveFormattingElements() - p.addElement() - case a.Rp, a.Rt: - if p.elementInScope(defaultScope, a.Ruby) { - p.generateImpliedEndTags() - } - p.addElement() - case a.Math, a.Svg: - p.reconstructActiveFormattingElements() - if p.tok.DataAtom == a.Math { - adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments) - } else { - adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments) - } - adjustForeignAttributes(p.tok.Attr) - p.addElement() - p.top().Namespace = p.tok.Data - if p.hasSelfClosingToken { - p.oe.pop() - p.acknowledgeSelfClosingTag() - } - return true - case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr: - // Ignore the token. - default: - p.reconstructActiveFormattingElements() - p.addElement() - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Body: - if p.elementInScope(defaultScope, a.Body) { - p.im = afterBodyIM - } - case a.Html: - if p.elementInScope(defaultScope, a.Body) { - p.parseImpliedToken(EndTagToken, a.Body, a.Body.String()) - return false - } - return true - case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul: - p.popUntil(defaultScope, p.tok.DataAtom) - case a.Form: - node := p.form - p.form = nil - i := p.indexOfElementInScope(defaultScope, a.Form) - if node == nil || i == -1 || p.oe[i] != node { - // Ignore the token. - return true - } - p.generateImpliedEndTags() - p.oe.remove(node) - case a.P: - if !p.elementInScope(buttonScope, a.P) { - p.parseImpliedToken(StartTagToken, a.P, a.P.String()) - } - p.popUntil(buttonScope, a.P) - case a.Li: - p.popUntil(listItemScope, a.Li) - case a.Dd, a.Dt: - p.popUntil(defaultScope, p.tok.DataAtom) - case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: - p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6) - case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U: - p.inBodyEndTagFormatting(p.tok.DataAtom) - case a.Applet, a.Marquee, a.Object: - if p.popUntil(defaultScope, p.tok.DataAtom) { - p.clearActiveFormattingElements() - } - case a.Br: - p.tok.Type = StartTagToken - return false - default: - p.inBodyEndTagOther(p.tok.DataAtom) - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - } - - return true -} - -func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { - // This is the "adoption agency" algorithm, described at - // https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency - - // TODO: this is a fairly literal line-by-line translation of that algorithm. - // Once the code successfully parses the comprehensive test suite, we should - // refactor this code to be more idiomatic. - - // Steps 1-4. The outer loop. - for i := 0; i < 8; i++ { - // Step 5. Find the formatting element. - var formattingElement *Node - for j := len(p.afe) - 1; j >= 0; j-- { - if p.afe[j].Type == scopeMarkerNode { - break - } - if p.afe[j].DataAtom == tagAtom { - formattingElement = p.afe[j] - break - } - } - if formattingElement == nil { - p.inBodyEndTagOther(tagAtom) - return - } - feIndex := p.oe.index(formattingElement) - if feIndex == -1 { - p.afe.remove(formattingElement) - return - } - if !p.elementInScope(defaultScope, tagAtom) { - // Ignore the tag. - return - } - - // Steps 9-10. Find the furthest block. - var furthestBlock *Node - for _, e := range p.oe[feIndex:] { - if isSpecialElement(e) { - furthestBlock = e - break - } - } - if furthestBlock == nil { - e := p.oe.pop() - for e != formattingElement { - e = p.oe.pop() - } - p.afe.remove(e) - return - } - - // Steps 11-12. Find the common ancestor and bookmark node. - commonAncestor := p.oe[feIndex-1] - bookmark := p.afe.index(formattingElement) - - // Step 13. The inner loop. Find the lastNode to reparent. - lastNode := furthestBlock - node := furthestBlock - x := p.oe.index(node) - // Steps 13.1-13.2 - for j := 0; j < 3; j++ { - // Step 13.3. - x-- - node = p.oe[x] - // Step 13.4 - 13.5. - if p.afe.index(node) == -1 { - p.oe.remove(node) - continue - } - // Step 13.6. - if node == formattingElement { - break - } - // Step 13.7. - clone := node.clone() - p.afe[p.afe.index(node)] = clone - p.oe[p.oe.index(node)] = clone - node = clone - // Step 13.8. - if lastNode == furthestBlock { - bookmark = p.afe.index(node) + 1 - } - // Step 13.9. - if lastNode.Parent != nil { - lastNode.Parent.RemoveChild(lastNode) - } - node.AppendChild(lastNode) - // Step 13.10. - lastNode = node - } - - // Step 14. Reparent lastNode to the common ancestor, - // or for misnested table nodes, to the foster parent. - if lastNode.Parent != nil { - lastNode.Parent.RemoveChild(lastNode) - } - switch commonAncestor.DataAtom { - case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: - p.fosterParent(lastNode) - default: - commonAncestor.AppendChild(lastNode) - } - - // Steps 15-17. Reparent nodes from the furthest block's children - // to a clone of the formatting element. - clone := formattingElement.clone() - reparentChildren(clone, furthestBlock) - furthestBlock.AppendChild(clone) - - // Step 18. Fix up the list of active formatting elements. - if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark { - // Move the bookmark with the rest of the list. - bookmark-- - } - p.afe.remove(formattingElement) - p.afe.insert(bookmark, clone) - - // Step 19. Fix up the stack of open elements. - p.oe.remove(formattingElement) - p.oe.insert(p.oe.index(furthestBlock)+1, clone) - } -} - -// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM. -// "Any other end tag" handling from 12.2.5.5 The rules for parsing tokens in foreign content -// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign -func (p *parser) inBodyEndTagOther(tagAtom a.Atom) { - for i := len(p.oe) - 1; i >= 0; i-- { - if p.oe[i].DataAtom == tagAtom { - p.oe = p.oe[:i] - break - } - if isSpecialElement(p.oe[i]) { - break - } - } -} - -// Section 12.2.5.4.8. -func textIM(p *parser) bool { - switch p.tok.Type { - case ErrorToken: - p.oe.pop() - case TextToken: - d := p.tok.Data - if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil { - // Ignore a newline at the start of a