├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── docker.yml │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── .golangci.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── _config.yml ├── collector ├── client.go ├── client_test.go ├── counters.go ├── infos.go ├── metrics.go └── service_times.go ├── config └── config.go ├── dashboards └── squid-sample-dashboard.json ├── deploy.sh ├── go.mod ├── go.sum ├── helm ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ ├── prometheusrules.yaml │ ├── secret.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── servicemonitor.yaml └── values.yaml ├── helpers.go ├── helpers_test.go ├── main.go ├── prometheus └── prometheus.yml └── types └── types.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | dashboards 4 | helm 5 | prometheus 6 | vendor 7 | Makefile 8 | Dockerfile -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [boynux] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: boynux 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | 11 | **To Reproduce** 12 | Steps to reproduce the behavior: 13 | 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | 19 | **OS (please complete the following information):** 20 | - OS: [e.g. Ubuntu] 21 | - Version [e.g. v0.4] 22 | 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build multi-arch docker images 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: ['v*'] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: Build and publish 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - name: Docker meta 30 | id: meta 31 | uses: docker/metadata-action@v5 32 | with: 33 | images: boynux/squid-exporter 34 | 35 | - name: Build and push 36 | uses: docker/build-push-action@v5 37 | with: 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | platforms: linux/amd64,linux/arm64,linux/arm/v7 41 | pull: true 42 | push: true 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | branches: 8 | - "master" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | arch: [386, amd64, arm64] 17 | include: 18 | - arch: arm 19 | armver: 6 20 | - arch: arm 21 | armver: 7 22 | env: 23 | arch_name: ${{ matrix.arch }}${{ matrix.arch == 'arm' && 'v' || '' }}${{ matrix.armver }} 24 | name: Build for ${{ matrix.arch }}${{ matrix.arch == 'arm' && 'v' || '' }}${{ matrix.armver }} 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Go environment 31 | uses: actions/setup-go@v4 32 | with: 33 | # Path to the go.mod or go.work file. 34 | go-version-file: go.mod 35 | 36 | - name: Build ${{ env.arch_name }} binary 37 | run: make build 38 | env: 39 | GOARCH: ${{ matrix.arch }} 40 | GOARM: ${{ matrix.armver }} 41 | 42 | - name: Rename ${{ env.arch_name }} binary 43 | run: mv bin/squid-exporter bin/squid-exporter-linux-${{ env.arch_name }} 44 | 45 | - name: Upload binary 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: squid-exporter-${{ strategy.job-index }} 49 | path: bin/squid-exporter-linux-${{ env.arch_name }} 50 | overwrite: true 51 | 52 | release: 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - name: Download binaries for release 57 | uses: actions/download-artifact@v4 58 | with: 59 | pattern: squid-exporter-* 60 | merge-multiple: true 61 | path: bin 62 | 63 | - name: Release latest version 64 | uses: svenstaro/upload-release-action@v2 65 | with: 66 | repo_token: ${{ secrets.GITHUB_TOKEN }} 67 | file: bin/squid-exporter-* 68 | file_glob: true 69 | tag: unstable 70 | overwrite: true 71 | body: "pre-release" 72 | prerelease: true 73 | 74 | - name: Release tag version 75 | if: startsWith(github.ref, 'refs/tags/v') 76 | uses: svenstaro/upload-release-action@v2 77 | with: 78 | repo_token: ${{ secrets.GITHUB_TOKEN }} 79 | file: bin/squid-exporter-* 80 | file_glob: true 81 | tag: ${{ github.ref }} 82 | overwrite: true 83 | body: "Stable release" 84 | 85 | needs: [build] 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | squid-exporter 3 | *.swp 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: go get && go build ./... && go test ./... && make 3 | command: go run 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - errorlint 4 | - goimports 5 | - misspell 6 | - perfsprint 7 | - testifylint 8 | - usestdlibvars 9 | 10 | issues: 11 | exclude-rules: 12 | - path: _test.go 13 | linters: 14 | - errcheck 15 | 16 | linters-settings: 17 | goimports: 18 | local-prefixes: github.com/boynux/squid-exporter 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | services: 3 | - docker 4 | arch: 5 | - amd64 6 | os: linux 7 | dist: xenial 8 | go: 9 | - 1.16.x 10 | deploy: 11 | - provider: script 12 | on: 13 | tags: true 14 | script: bash ./deploy.sh 15 | - provider: releases 16 | skip_cleanup: true 17 | token: $GH_TOKEN 18 | file: bin/squid-exporter 19 | on: 20 | tags: true 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at boynux@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as build 2 | 3 | ARG TARGETPLATFORM 4 | RUN echo "Building for ${TARGETPLATFORM}" 5 | 6 | WORKDIR /go/src 7 | 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | 11 | COPY . . 12 | 13 | RUN CGO_ENABLED=0 go build -a -ldflags '-extldflags "-s -w -static"' -o /squid-exporter . 14 | 15 | 16 | FROM gcr.io/distroless/static:nonroot as final 17 | 18 | LABEL org.opencontainers.image.title="Squid Exporter" 19 | LABEL org.opencontainers.image.description="This is a Docker image for Squid Prometheus Exporter." 20 | LABEL org.opencontainers.image.source="https://github.com/boynux/squid-exporter/" 21 | LABEL org.opencontainers.image.licenses="MIT" 22 | 23 | ENV SQUID_EXPORTER_LISTEN="0.0.0.0:9301" 24 | 25 | COPY --from=build /squid-exporter /usr/local/bin/squid-exporter 26 | # Allow /etc/hosts to be used for DNS 27 | COPY --from=build /etc/nsswitch.conf /etc/nsswitch.conf 28 | 29 | EXPOSE 9301 30 | 31 | ENTRYPOINT ["/usr/local/bin/squid-exporter"] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mohammad Arab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all fmt vet test build docker clean 2 | 3 | all: fmt vet test build 4 | 5 | EXE = ./bin/squid-exporter 6 | SRC = $(shell find . -type f -name '*.go') 7 | VERSION ?= $(shell cat VERSION) 8 | REVISION = $(shell git rev-parse HEAD) 9 | BRANCH = $(shell git rev-parse --abbrev-ref HEAD) 10 | LDFLAGS = -extldflags "-s -w -static" \ 11 | -X github.com/prometheus/common/version.Version=$(VERSION) \ 12 | -X github.com/prometheus/common/version.Revision=$(REVISION) \ 13 | -X github.com/prometheus/common/version.Branch=$(BRANCH) 14 | 15 | $(EXE): $(SRC) 16 | CGO_ENABLED=0 GOOS=linux go build -a -ldflags '$(LDFLAGS)' -o $(EXE) . 17 | 18 | fmt: 19 | go fmt ./... 20 | 21 | vet: 22 | go vet ./... 23 | 24 | test: 25 | go test -v ./... 26 | 27 | build: $(EXE) 28 | 29 | docker: 30 | docker build -t squid-exporter . 31 | 32 | clean: 33 | rm -f $(EXE) 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/boynux/squid-exporter) 2 | 3 | [![Github Actions](https://github.com/boynux/squid-exporter/actions/workflows/release.yml/badge.svg)](https://github.com/boynux/squid-exporter/actions/workflows/release.yml) 4 | [![Github Docker](https://github.com/boynux/squid-exporter/actions/workflows/docker.yml/badge.svg)](https://github.com/boynux/squid-exporter/actions/workflows/docker.yml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/boynux/squid-exporter)](https://goreportcard.com/report/github.com/boynux/squid-exporter) 6 | [![Maintainability](https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability)](https://codeclimate.com/github/boynux/squid-exporter) 7 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3TH7YAMMEC5L4&source=url) 8 | 9 | **Note: I've been very busy on the past couple of months with my personal life and work. Thanks for filing issues and feature requests. I'll start to go through them and provide updates very soon.** 10 | 11 | 12 | Squid Prometheus exporter 13 | -------------------------- 14 | 15 | Exports squid metrics in Prometheus format 16 | 17 | **NOTE**: From release 1.0 metric names and some parameters has changed. Make sure you check the docs and update your deployments accordingly! 18 | 19 | New 20 | ----- 21 | 22 | * Using environment variables to configure the exporter 23 | * Adding custom labels to metrics 24 | * Enabling TLS for exporter via [WebConfig](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md) 25 | 26 | Usage: 27 | ------ 28 | Simple usage: 29 | 30 | squid-exporter -squid-hostname "localhost" -squid-port 3128 31 | 32 | [Configure Prometheus](https://github.com/boynux/squid-exporter/blob/master/prometheus/prometheus.yml) to scrape metrics from `localhost:9301/metrics` 33 | 34 | - job_name: squid 35 | # squid-exporter is installed, grab stats about the local 36 | # squid instance. 37 | target_groups: 38 | - targets: ['localhost:9301'] 39 | 40 | To get all the parameteres, command line arguments always override default and environment variables configs: 41 | 42 | squid-exporter -help 43 | 44 | The following environment variables can be used to override default parameters: 45 | 46 | ``` 47 | SQUID_EXPORTER_LISTEN 48 | SQUID_EXPORTER_WEB_CONFIG_PATH 49 | SQUID_EXPORTER_METRICS_PATH 50 | SQUID_HOSTNAME 51 | SQUID_PORT 52 | SQUID_LOGIN 53 | SQUID_PASSWORD 54 | SQUID_EXTRACTSERVICETIMES 55 | ``` 56 | 57 | Usage with docker: 58 | ------ 59 | Basic setup assuming Squid is running on the same machine: 60 | 61 | docker run --net=host -d boynux/squid-exporter 62 | 63 | Setup with Squid running on a different host 64 | 65 | docker run -p 9301:9301 -d boynux/squid-exporter -squid-hostname "192.168.0.2" -squid-port 3128 -listen ":9301" 66 | 67 | With environment variables 68 | 69 | docker run -p 9301:9301 -d -e SQUID_PORT="3128" -e SQUID_HOSTNAME="192.168.0.2" -e SQUID_EXPORTER_LISTEN=":9301" boynux/squid-exporter 70 | 71 | 72 | Build: 73 | -------- 74 | 75 | This project is written in Go, so all the usual methods for building (or cross compiling) a Go application would work. 76 | 77 | If you are not very familiar with Go you can download the binary from [releases](https://github.com/boynux/squid-exporter/releases). 78 | 79 | Or build it for your OS: 80 | 81 | `go install https://github.com/boynux/squid-exporter` 82 | 83 | then you can find the binary in: `$GOPATH/bin/squid-exporter` 84 | 85 | Features: 86 | --------- 87 | 88 | - [ ] Expose Squid counters 89 | - [x] Client HTTP 90 | - [x] Server HTTP 91 | - [x] Server ALL 92 | - [x] Server FTP 93 | - [x] Server Other 94 | - [ ] ICP 95 | - [ ] CD 96 | - [x] Swap 97 | - [ ] Page Faults 98 | - [ ] Others 99 | - [ ] Expose Squid service times 100 | - [x] HTTP requests 101 | - [x] Cache misses 102 | - [x] Cache hits 103 | - [x] Near hits 104 | - [ ] Not-Modified replies 105 | - [x] DNS lookups 106 | - [ ] ICP queries 107 | - [ ] Expose squid Info 108 | - [x] Squid service info (as label) 109 | - [x] Connection information for squid 110 | - [x] Cache information for squid 111 | - [ ] Median Service Times (seconds) 5 min 112 | - [x] Resource usage for squid 113 | - [x] Memory accounted for 114 | - [x] File descriptor usage for squid 115 | - [x] Internal Data Structures 116 | - [ ] Histograms 117 | - [ ] Other metrics 118 | - [x] Squid Authentication (Basic Auth) 119 | 120 | FAQ: 121 | -------- 122 | 123 | - Q: Which versions of Squid are supported? 124 | - A: Squid version 3.2.0.10 and later. 125 | 126 | - Q: Why are no Squid metrics reported by the exporter? 127 | - A: This usually means that the exporter cannot reach the Squid server, or that the cachemgr ACLs 128 | are incorrect. To debug and mitigate: 129 | - First make sure that the host running the exporter can access the Squid cachemgr URL, e.g.: 130 | `curl http://localhost:3128/squid-internal-mgr/info` 131 | 132 | - If that fails, verify that the Squid ACL configuration allows the exporter host to access the 133 | cachemgr. In the following example, a custom `prometheus` ACL is defined to allow cachemgr 134 | endpoint access from outside the Squid host, via the network (use with caution). 135 | ``` 136 | acl prometheus src 192.0.2.0/24 137 | http_access allow localhost manager 138 | http_access allow prometheus manager 139 | http_access deny manager 140 | ``` 141 | 142 | - Q: Why `process_open_fds` metric is not exported? 143 | - A: This usualy means exporter don't have permission to read `/proc//fd` folder. You can either 144 | 145 | 1. _[recommended]_ Set `CAP_DAC_READ_SEARCH` capability for squid exporter process (or docker). (eg. `sudo setcap 'cap_dac_read_search+ep' ./bin/squid-exporter`) 146 | 2. _[not recommended]_ Run the exporter as root. 147 | 148 | Contribution: 149 | ------------- 150 | 151 | Pull request and issues are very welcome. 152 | 153 | If you found this program useful please consider donations [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3TH7YAMMEC5L4&source=url) 154 | 155 | Copyright: 156 | ---------- 157 | 158 | [MIT License](https://opensource.org/licenses/MIT) 159 | 160 | 161 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.13.0 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /collector/client.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/boynux/squid-exporter/types" 13 | ) 14 | 15 | // CacheObjectClient holds information about Squid manager. 16 | type CacheObjectClient struct { 17 | baseURL string 18 | username string 19 | password string 20 | proxyHeader string 21 | } 22 | 23 | // SquidClient provides functionality to fetch Squid metrics. 24 | type SquidClient interface { 25 | GetCounters() (types.Counters, error) 26 | GetServiceTimes() (types.Counters, error) 27 | GetInfos() (types.Counters, error) 28 | } 29 | 30 | // NewCacheObjectClient initializes a new cache client. 31 | func NewCacheObjectClient(baseURL, username, password, proxyHeader string) *CacheObjectClient { 32 | return &CacheObjectClient{ 33 | baseURL, 34 | username, 35 | password, 36 | proxyHeader, 37 | } 38 | } 39 | 40 | func (c *CacheObjectClient) readFromSquid(endpoint string) (*bufio.Reader, error) { 41 | req, err := http.NewRequest(http.MethodGet, c.baseURL+endpoint, nil) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | if c.username != "" && c.password != "" { 47 | req.SetBasicAuth(c.username, c.password) 48 | } 49 | 50 | resp, err := http.DefaultClient.Do(req) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if resp.StatusCode != http.StatusOK { 56 | return nil, fmt.Errorf("non-success code %d while fetching metrics", resp.StatusCode) 57 | } 58 | 59 | return bufio.NewReader(resp.Body), err 60 | } 61 | 62 | // GetCounters fetches counters from Squid cache manager. 63 | func (c *CacheObjectClient) GetCounters() (types.Counters, error) { 64 | var counters types.Counters 65 | 66 | reader, err := c.readFromSquid("counters") 67 | if err != nil { 68 | return nil, fmt.Errorf("error getting counters: %w", err) 69 | } 70 | 71 | scanner := bufio.NewScanner(reader) 72 | for scanner.Scan() { 73 | if c, err := decodeCounterStrings(scanner.Text()); err != nil { 74 | log.Println(err) 75 | } else { 76 | counters = append(counters, c) 77 | } 78 | } 79 | if err := scanner.Err(); err != nil { 80 | return nil, err 81 | } 82 | 83 | return counters, err 84 | } 85 | 86 | // GetServiceTimes fetches service times from Squid cache manager. 87 | func (c *CacheObjectClient) GetServiceTimes() (types.Counters, error) { 88 | var serviceTimes types.Counters 89 | 90 | reader, err := c.readFromSquid("service_times") 91 | if err != nil { 92 | return nil, fmt.Errorf("error getting service times: %w", err) 93 | } 94 | 95 | scanner := bufio.NewScanner(reader) 96 | for scanner.Scan() { 97 | if s, err := decodeServiceTimeStrings(scanner.Text()); err != nil { 98 | log.Println(err) 99 | } else if s.Key != "" { 100 | serviceTimes = append(serviceTimes, s) 101 | } 102 | } 103 | if err := scanner.Err(); err != nil { 104 | return nil, err 105 | } 106 | 107 | return serviceTimes, err 108 | } 109 | 110 | // GetInfos fetches info from Squid cache manager. 111 | func (c *CacheObjectClient) GetInfos() (types.Counters, error) { 112 | var infos types.Counters 113 | 114 | reader, err := c.readFromSquid("info") 115 | if err != nil { 116 | return nil, fmt.Errorf("error getting info: %w", err) 117 | } 118 | 119 | infoVarLabels := types.Counter{Key: "squid_info", Value: 1} 120 | 121 | scanner := bufio.NewScanner(reader) 122 | for scanner.Scan() { 123 | if dis, err := decodeInfoStrings(scanner.Text()); err != nil { 124 | log.Println(err) 125 | } else { 126 | if len(dis.VarLabels) > 0 { 127 | if dis.VarLabels[0].Key == "5min" { 128 | var infoAvg5, infoAvg60 types.Counter 129 | 130 | infoAvg5.Key = dis.Key + "_" + dis.VarLabels[0].Key 131 | infoAvg60.Key = dis.Key + "_" + dis.VarLabels[1].Key 132 | 133 | if value, err := strconv.ParseFloat(dis.VarLabels[0].Value, 64); err == nil { 134 | infoAvg5.Value = value 135 | infos = append(infos, infoAvg5) 136 | } 137 | if value, err := strconv.ParseFloat(dis.VarLabels[1].Value, 64); err == nil { 138 | infoAvg60.Value = value 139 | infos = append(infos, infoAvg60) 140 | } 141 | } else { 142 | infoVarLabels.VarLabels = append(infoVarLabels.VarLabels, dis.VarLabels[0]) 143 | } 144 | } else if dis.Key != "" { 145 | infos = append(infos, dis) 146 | } 147 | } 148 | } 149 | if err := scanner.Err(); err != nil { 150 | return nil, err 151 | } 152 | 153 | infos = append(infos, infoVarLabels) 154 | return infos, err 155 | } 156 | 157 | func decodeCounterStrings(line string) (types.Counter, error) { 158 | if equal := strings.Index(line, "="); equal >= 0 { 159 | if key := strings.TrimSpace(line[:equal]); len(key) > 0 { 160 | value := "" 161 | if len(line) > equal { 162 | value = strings.TrimSpace(line[equal+1:]) 163 | } 164 | 165 | // Remove additional formating string from `sample_time` 166 | if slices := strings.Split(value, " "); len(slices) > 0 { 167 | value = slices[0] 168 | } 169 | 170 | if i, err := strconv.ParseFloat(value, 64); err == nil { 171 | return types.Counter{Key: key, Value: i}, nil 172 | } 173 | } 174 | } 175 | 176 | return types.Counter{}, errors.New("counter - could not parse line: " + line) 177 | } 178 | 179 | func decodeServiceTimeStrings(line string) (types.Counter, error) { 180 | if strings.HasSuffix(line, ":") { // A header line isn't a metric 181 | return types.Counter{}, nil 182 | } 183 | if equal := strings.Index(line, ":"); equal >= 0 { 184 | if key := strings.TrimSpace(line[:equal]); len(key) > 0 { 185 | value := "" 186 | if len(line) > equal { 187 | value = strings.TrimSpace(line[equal+1:]) 188 | } 189 | key = strings.Replace(key, " ", "_", -1) 190 | key = strings.Replace(key, "(", "", -1) 191 | key = strings.Replace(key, ")", "", -1) 192 | 193 | if equalTwo := strings.Index(value, "%"); equalTwo >= 0 { 194 | if keyTwo := strings.TrimSpace(value[:equalTwo]); len(keyTwo) > 0 { 195 | if len(value) > equalTwo { 196 | value = strings.Split(strings.TrimSpace(value[equalTwo+1:]), " ")[0] 197 | } 198 | key = key + "_" + keyTwo 199 | } 200 | } 201 | 202 | if value, err := strconv.ParseFloat(value, 64); err == nil { 203 | return types.Counter{Key: key, Value: value}, nil 204 | } 205 | } 206 | } 207 | 208 | return types.Counter{}, errors.New("service times - could not parse line: " + line) 209 | } 210 | 211 | func decodeInfoStrings(line string) (types.Counter, error) { 212 | if strings.HasSuffix(line, ":") { // A header line isn't a metric 213 | return types.Counter{}, nil 214 | } 215 | 216 | if idx := strings.Index(line, ":"); idx >= 0 { // detect if line contain metric format like "metricName: value" 217 | if key := strings.TrimSpace(line[:idx]); len(key) > 0 { 218 | value := "" 219 | if len(line) > idx { 220 | value = strings.TrimSpace(line[idx+1:]) 221 | } 222 | key = strings.Replace(key, " ", "_", -1) 223 | key = strings.Replace(key, "(", "", -1) 224 | key = strings.Replace(key, ")", "", -1) 225 | key = strings.Replace(key, ",", "", -1) 226 | key = strings.Replace(key, "/", "", -1) 227 | 228 | // metrics with value as string need to save as label, format like "Squid Object Cache: Version 6.1" (the 3 first metrics) 229 | if key == "Squid_Object_Cache" || key == "Build_Info" || key == "Service_Name" { 230 | if key == "Squid_Object_Cache" { // To clarify that the value is the squid version. 231 | key += "_Version" 232 | if slices := strings.Split(value, " "); len(slices) > 0 { 233 | value = slices[1] 234 | } 235 | } 236 | var infoVarLabel types.VarLabel 237 | infoVarLabel.Key = key 238 | infoVarLabel.Value = value 239 | 240 | var infoCounter types.Counter 241 | infoCounter.Key = key 242 | infoCounter.VarLabels = append(infoCounter.VarLabels, infoVarLabel) 243 | return infoCounter, nil 244 | } else if key == "Start_Time" || key == "Current_Time" { // discart this metrics 245 | return types.Counter{}, nil 246 | } 247 | 248 | // Remove additional information in value metric 249 | if slices := strings.Split(value, " "); len(slices) > 0 { 250 | if slices[0] == "5min:" && slices[2] == "60min:" { // catch metrics with avg in 5min and 60min format like "Hits as % of bytes sent: 5min: -0.0%, 60min: -0.0%" 251 | var infoAvg5mVarLabel types.VarLabel 252 | infoAvg5mVarLabel.Key = slices[0] 253 | infoAvg5mVarLabel.Value = slices[1] 254 | 255 | infoAvg5mVarLabel.Key = strings.Replace(infoAvg5mVarLabel.Key, ":", "", -1) 256 | infoAvg5mVarLabel.Value = strings.Replace(infoAvg5mVarLabel.Value, "%", "", -1) 257 | infoAvg5mVarLabel.Value = strings.Replace(infoAvg5mVarLabel.Value, ",", "", -1) 258 | 259 | var infoAvg60mVarLabel types.VarLabel 260 | infoAvg60mVarLabel.Key = slices[2] 261 | infoAvg60mVarLabel.Value = slices[3] 262 | 263 | infoAvg60mVarLabel.Key = strings.Replace(infoAvg60mVarLabel.Key, ":", "", -1) 264 | infoAvg60mVarLabel.Value = strings.Replace(infoAvg60mVarLabel.Value, "%", "", -1) 265 | infoAvg60mVarLabel.Value = strings.Replace(infoAvg60mVarLabel.Value, ",", "", -1) 266 | 267 | var infoAvgCounter types.Counter 268 | infoAvgCounter.Key = key 269 | infoAvgCounter.VarLabels = append(infoAvgCounter.VarLabels, infoAvg5mVarLabel, infoAvg60mVarLabel) 270 | 271 | return infoAvgCounter, nil 272 | } 273 | 274 | value = slices[0] 275 | } 276 | 277 | value = strings.Replace(value, "%", "", -1) 278 | value = strings.Replace(value, ",", "", -1) 279 | 280 | if i, err := strconv.ParseFloat(value, 64); err == nil { 281 | return types.Counter{Key: key, Value: i}, nil 282 | } 283 | } 284 | } else { 285 | // this catch the last 4 metrics format like "value metricName" 286 | lineTrimed := strings.TrimSpace(line) 287 | 288 | if idx := strings.Index(lineTrimed, " "); idx >= 0 { 289 | key := strings.TrimSpace(lineTrimed[idx+1:]) 290 | key = strings.Replace(key, " ", "_", -1) 291 | key = strings.Replace(key, "-", "_", -1) 292 | 293 | value := strings.TrimSpace(lineTrimed[:idx]) 294 | 295 | if i, err := strconv.ParseFloat(value, 64); err == nil { 296 | return types.Counter{Key: key, Value: i}, nil 297 | } 298 | } 299 | } 300 | 301 | return types.Counter{}, errors.New("Info - could not parse line: " + line) 302 | } 303 | -------------------------------------------------------------------------------- /collector/client_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/boynux/squid-exporter/types" 12 | ) 13 | 14 | func TestReadFromSquid(t *testing.T) { 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | assert.Equal(t, r.RequestURI, "/squid-internal-mgr/test") 17 | })) 18 | defer ts.Close() 19 | 20 | coc := &CacheObjectClient{ 21 | ts.URL + "/squid-internal-mgr/", 22 | "", 23 | "", 24 | "", 25 | } 26 | 27 | _, err := coc.readFromSquid("test") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func TestDecodeMetricStrings(t *testing.T) { 34 | tests := []struct { 35 | s string 36 | c types.Counter 37 | e string 38 | d func(string) (types.Counter, error) 39 | }{ 40 | {"swap.files_cleaned=1", types.Counter{Key: "swap.files_cleaned", Value: 1}, "", decodeCounterStrings}, 41 | {"client.http_requests=1", types.Counter{Key: "client.http_requests", Value: 1}, "", decodeCounterStrings}, 42 | {"# test for invalid metric line", types.Counter{}, "counter - could not parse line: # test for invalid metric line", decodeCounterStrings}, 43 | 44 | {" HTTP Requests (All): 70% 10.00000 9.50000\n", types.Counter{Key: "HTTP_Requests_All_70", Value: 10}, "", decodeServiceTimeStrings}, 45 | {" Not-Modified Replies: 5% 12.00000 10.00000\n", types.Counter{Key: "Not-Modified_Replies_5", Value: 12}, "", decodeServiceTimeStrings}, 46 | {" ICP Queries: 85% 900.00000 1200.00000\n", types.Counter{Key: "ICP_Queries_85", Value: 900}, "", decodeServiceTimeStrings}, 47 | } 48 | 49 | for _, tc := range tests { 50 | c, err := tc.d(tc.s) 51 | 52 | if tc.e != "" { 53 | require.EqualError(t, err, tc.e) 54 | } 55 | assert.Equal(t, tc.c, c) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /collector/counters.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | type squidCounter struct { 11 | Section string 12 | Counter string 13 | Suffix string 14 | Description string 15 | } 16 | 17 | var squidCounters = []squidCounter{ 18 | {"client_http", "requests", "total", "The total number of client requests"}, 19 | {"client_http", "hits", "total", "The total number of client cache hits"}, 20 | {"client_http", "errors", "total", "The total number of client http errors"}, 21 | {"client_http", "kbytes_in", "kbytes_total", "The total number of client kbytes received"}, 22 | {"client_http", "kbytes_out", "kbytes_total", "The total number of client kbytes transferred"}, 23 | {"client_http", "hit_kbytes_out", "bytes_total", "The total number of client kbytes cache hit"}, 24 | 25 | {"server.http", "requests", "total", "The total number of server http requests"}, 26 | {"server.http", "errors", "total", "The total number of server http errors"}, 27 | {"server.http", "kbytes_in", "kbytes_total", "The total number of server http kbytes received"}, 28 | {"server.http", "kbytes_out", "kbytes_total", "The total number of server http kbytes transferred"}, 29 | 30 | {"server.all", "requests", "total", "The total number of server all requests"}, 31 | {"server.all", "errors", "total", "The total number of server all errors"}, 32 | {"server.all", "kbytes_in", "kbytes_total", "The total number of server kbytes received"}, 33 | {"server.all", "kbytes_out", "kbytes_total", "The total number of server kbytes transferred"}, 34 | 35 | {"server.ftp", "requests", "total", "The total number of server ftp requests"}, 36 | {"server.ftp", "errors", "total", "The total number of server ftp errors"}, 37 | {"server.ftp", "kbytes_in", "kbytes_total", "The total number of server ftp kbytes received"}, 38 | {"server.ftp", "kbytes_out", "kbytes_total", "The total number of server ftp kbytes transferred"}, 39 | 40 | {"server.other", "requests", "total", "The total number of server other requests"}, 41 | {"server.other", "errors", "total", "The total number of server other errors"}, 42 | {"server.other", "kbytes_in", "kbytes_total", "The total number of server other kbytes received"}, 43 | {"server.other", "kbytes_out", "kbytes_total", "The total number of server other kbytes transferred"}, 44 | 45 | {"swap", "ins", "total", "The number of objects read from disk"}, 46 | {"swap", "outs", "total", "The number of objects saved to disk"}, 47 | {"swap", "files_cleaned", "total", "The number of orphaned cache files removed by the periodic cleanup procedure"}, 48 | } 49 | 50 | func generateSquidCounters(labels []string) descMap { 51 | counters := descMap{} 52 | 53 | for i := range squidCounters { 54 | counter := squidCounters[i] 55 | 56 | counters[fmt.Sprintf("%s.%s", counter.Section, counter.Counter)] = prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, strings.Replace(counter.Section, ".", "_", -1), 58 | fmt.Sprintf("%s_%s", counter.Counter, counter.Suffix)), 59 | counter.Description, 60 | labels, nil, 61 | ) 62 | } 63 | 64 | return counters 65 | } 66 | -------------------------------------------------------------------------------- /collector/infos.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | type squidInfos struct { 10 | Section string 11 | Description string 12 | Unit string 13 | } 14 | 15 | var squidInfoss = []squidInfos{ 16 | {"Number_of_clients_accessing_cache", "", "number"}, 17 | {"Number_of_HTTP_requests_received", "", "number"}, 18 | {"Number_of_ICP_messages_received", "", "number"}, 19 | {"Number_of_ICP_messages_sent", "", "number"}, 20 | {"Number_of_queued_ICP_replies", "", "number"}, 21 | {"Number_of_HTCP_messages_received", "", "number"}, 22 | {"Number_of_HTCP_messages_sent", "", "number"}, 23 | {"Request_failure_ratio", "", "%"}, 24 | {"Average_HTTP_requests_per_minute_since_start", "", "%"}, 25 | {"Average_ICP_messages_per_minute_since_start", "", "%"}, 26 | {"Select_loop_called", "", "number"}, 27 | {"Hits_as_%_of_all_requests_5min", "", "%"}, 28 | {"Hits_as_%_of_bytes_sent_5min", "", "%"}, 29 | {"Memory_hits_as_%_of_hit_requests_5min", "", "%"}, 30 | {"Disk_hits_as_%_of_hit_requests_5min", "", "%"}, 31 | {"Hits_as_%_of_all_requests_60min", "", "%"}, 32 | {"Hits_as_%_of_bytes_sent_60min", "", "%"}, 33 | {"Memory_hits_as_%_of_hit_requests_60min", "", "%"}, 34 | {"Disk_hits_as_%_of_hit_requests_60min", "", "%"}, 35 | {"Storage_Swap_size", "", "KB"}, 36 | {"Storage_Swap_capacity", "", "% use"}, 37 | {"Storage_Mem_size", "", "KB"}, 38 | {"Storage_Mem_capacity", "", "% used"}, 39 | {"Mean_Object_Size", "", "KB"}, 40 | {"Requests_given_to_unlinkd", "", "number"}, 41 | {"UP_Time", "time squid is up", "seconds"}, 42 | {"CPU_Time", "", "seconds"}, 43 | {"CPU_Usage", "of cpu usage", "%"}, 44 | {"CPU_Usage_5_minute_avg", "of cpu usage", "%"}, 45 | {"CPU_Usage_60_minute_avg", "of cpu usage", "%"}, 46 | {"Maximum_Resident_Size", "", "KB"}, 47 | {"Page_faults_with_physical_i_o", "", "number"}, 48 | {"Total_accounted", "", "KB"}, 49 | {"memPoolAlloc_calls", "", "number"}, 50 | {"memPoolFree_calls", "", "number"}, 51 | {"Maximum_number_of_file_descriptors", "", "number"}, 52 | {"Largest_file_desc_currently_in_use", "", "number"}, 53 | {"Number_of_file_desc_currently_in_use", "", "number"}, 54 | {"Files_queued_for_open", "", "number"}, 55 | {"Available_number_of_file_descriptors", "", "number"}, 56 | {"Reserved_number_of_file_descriptors", "", "number"}, 57 | {"Store_Disk_files_open", "", "number"}, 58 | {"StoreEntries", "", "number"}, 59 | {"StoreEntries_with_MemObjects", "", "number"}, 60 | {"Hot_Object_Cache_Items", "", "number"}, 61 | {"on_disk_objects", "", "number"}, 62 | } 63 | 64 | func generateSquidInfos(labels []string) descMap { 65 | infos := descMap{} 66 | 67 | for i := range squidInfoss { 68 | info := squidInfoss[i] 69 | 70 | var key string 71 | var name string 72 | var description string 73 | 74 | key = info.Section 75 | name = prometheus.BuildFQName(namespace, "info", strings.Replace(info.Section, "%", "pct", -1)) 76 | 77 | if info.Description == "" { 78 | description = strings.Replace(info.Section, "_", " ", -1) 79 | } else { 80 | description = info.Description 81 | } 82 | 83 | description = description + " in " + info.Unit 84 | 85 | infos[key] = prometheus.NewDesc( 86 | name, 87 | description, 88 | labels, nil, 89 | ) 90 | } 91 | 92 | return infos 93 | } 94 | -------------------------------------------------------------------------------- /collector/metrics.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | 10 | "github.com/boynux/squid-exporter/config" 11 | ) 12 | 13 | type descMap map[string]*prometheus.Desc 14 | 15 | const ( 16 | namespace = "squid" 17 | timeout = 10 * time.Second 18 | ) 19 | 20 | var ( 21 | counters descMap 22 | serviceTimes descMap // ExtractServiceTimes decides if we want to extract service times 23 | ExtractServiceTimes bool 24 | infos descMap 25 | ) 26 | 27 | // Exporter entry point to Squid exporter. 28 | type Exporter struct { 29 | client SquidClient 30 | 31 | hostname string 32 | port int 33 | 34 | labels config.Labels 35 | up *prometheus.GaugeVec 36 | } 37 | 38 | type CollectorConfig struct { 39 | Hostname string 40 | Port int 41 | Login string 42 | Password string 43 | Labels config.Labels 44 | ProxyHeader string 45 | } 46 | 47 | // New initializes a new exporter. 48 | func New(c *CollectorConfig) *Exporter { 49 | counters = generateSquidCounters(c.Labels.Keys) 50 | if ExtractServiceTimes { 51 | serviceTimes = generateSquidServiceTimes(c.Labels.Keys) 52 | } 53 | 54 | infos = generateSquidInfos(c.Labels.Keys) 55 | 56 | return &Exporter{ 57 | NewCacheObjectClient(fmt.Sprintf("http://%s:%d/squid-internal-mgr/", c.Hostname, c.Port), c.Login, c.Password, c.ProxyHeader), 58 | 59 | c.Hostname, 60 | c.Port, 61 | 62 | c.Labels, 63 | prometheus.NewGaugeVec(prometheus.GaugeOpts{ 64 | Namespace: namespace, 65 | Name: "up", 66 | Help: "Was the last query of squid successful?", 67 | }, []string{"host"}), 68 | } 69 | } 70 | 71 | // Describe describes all the metrics ever exported by the ECS exporter. It 72 | // implements prometheus.Collector. 73 | func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { 74 | e.up.Describe(ch) 75 | 76 | for _, v := range counters { 77 | ch <- v 78 | } 79 | 80 | if ExtractServiceTimes { 81 | for _, v := range serviceTimes { 82 | ch <- v 83 | } 84 | } 85 | 86 | for _, v := range infos { 87 | ch <- v 88 | } 89 | } 90 | 91 | // Collect fetches metrics from Squid manager and expose them to Prometheus. 92 | func (e *Exporter) Collect(c chan<- prometheus.Metric) { 93 | insts, err := e.client.GetCounters() 94 | 95 | if err == nil { 96 | e.up.With(prometheus.Labels{"host": e.hostname}).Set(1) 97 | for i := range insts { 98 | if d, ok := counters[insts[i].Key]; ok { 99 | c <- prometheus.MustNewConstMetric(d, prometheus.CounterValue, insts[i].Value, e.labels.Values...) 100 | } 101 | } 102 | } else { 103 | e.up.With(prometheus.Labels{"host": e.hostname}).Set(0) 104 | log.Println("Could not fetch counter metrics from squid instance: ", err) 105 | } 106 | 107 | if ExtractServiceTimes { 108 | insts, err = e.client.GetServiceTimes() 109 | 110 | if err == nil { 111 | for i := range insts { 112 | if d, ok := serviceTimes[insts[i].Key]; ok { 113 | c <- prometheus.MustNewConstMetric(d, prometheus.GaugeValue, insts[i].Value, e.labels.Values...) 114 | } 115 | } 116 | } else { 117 | log.Println("Could not fetch service times metrics from squid instance: ", err) 118 | } 119 | } 120 | 121 | insts, err = e.client.GetInfos() 122 | if err == nil { 123 | for i := range insts { 124 | if d, ok := infos[insts[i].Key]; ok { 125 | c <- prometheus.MustNewConstMetric(d, prometheus.GaugeValue, insts[i].Value, e.labels.Values...) 126 | } else if insts[i].Key == "squid_info" { 127 | infoMetricName := prometheus.BuildFQName(namespace, "info", "service") 128 | var labelsKeys []string 129 | var labelsValues []string 130 | 131 | for z := range insts[i].VarLabels { 132 | labelsKeys = append(labelsKeys, insts[i].VarLabels[z].Key) 133 | labelsValues = append(labelsValues, insts[i].VarLabels[z].Value) 134 | } 135 | 136 | infoDesc := prometheus.NewDesc( 137 | infoMetricName, 138 | "Metrics as string from info on cache_object", 139 | labelsKeys, 140 | nil, 141 | ) 142 | c <- prometheus.MustNewConstMetric(infoDesc, prometheus.GaugeValue, insts[i].Value, labelsValues...) 143 | } 144 | } 145 | } else { 146 | log.Println("Could not fetch info metrics from squid instance: ", err) 147 | } 148 | 149 | e.up.Collect(c) 150 | } 151 | -------------------------------------------------------------------------------- /collector/service_times.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | type squidServiceTimes struct { 11 | Section string 12 | Counter string 13 | Suffix string 14 | Description string 15 | } 16 | 17 | var squidServiceTimess = []squidServiceTimes{ 18 | {"HTTP_Requests", "All", "5", "Service Time Percentiles 5min"}, 19 | {"HTTP_Requests", "All", "10", "Service Time Percentiles 5min"}, 20 | {"HTTP_Requests", "All", "15", "Service Time Percentiles 5min"}, 21 | {"HTTP_Requests", "All", "20", "Service Time Percentiles 5min"}, 22 | {"HTTP_Requests", "All", "25", "Service Time Percentiles 5min"}, 23 | {"HTTP_Requests", "All", "30", "Service Time Percentiles 5min"}, 24 | {"HTTP_Requests", "All", "35", "Service Time Percentiles 5min"}, 25 | {"HTTP_Requests", "All", "40", "Service Time Percentiles 5min"}, 26 | {"HTTP_Requests", "All", "45", "Service Time Percentiles 5min"}, 27 | {"HTTP_Requests", "All", "50", "Service Time Percentiles 5min"}, 28 | {"HTTP_Requests", "All", "55", "Service Time Percentiles 5min"}, 29 | {"HTTP_Requests", "All", "60", "Service Time Percentiles 5min"}, 30 | {"HTTP_Requests", "All", "65", "Service Time Percentiles 5min"}, 31 | {"HTTP_Requests", "All", "70", "Service Time Percentiles 5min"}, 32 | {"HTTP_Requests", "All", "75", "Service Time Percentiles 5min"}, 33 | {"HTTP_Requests", "All", "80", "Service Time Percentiles 5min"}, 34 | {"HTTP_Requests", "All", "85", "Service Time Percentiles 5min"}, 35 | {"HTTP_Requests", "All", "90", "Service Time Percentiles 5min"}, 36 | {"HTTP_Requests", "All", "95", "Service Time Percentiles 5min"}, 37 | {"HTTP_Requests", "All", "100", "Service Time Percentiles 5min"}, 38 | {"Cache_Misses", "", "5", "Service Time Percentiles 5min"}, 39 | {"Cache_Misses", "", "10", "Service Time Percentiles 5min"}, 40 | {"Cache_Misses", "", "15", "Service Time Percentiles 5min"}, 41 | {"Cache_Misses", "", "20", "Service Time Percentiles 5min"}, 42 | {"Cache_Misses", "", "25", "Service Time Percentiles 5min"}, 43 | {"Cache_Misses", "", "30", "Service Time Percentiles 5min"}, 44 | {"Cache_Misses", "", "35", "Service Time Percentiles 5min"}, 45 | {"Cache_Misses", "", "40", "Service Time Percentiles 5min"}, 46 | {"Cache_Misses", "", "45", "Service Time Percentiles 5min"}, 47 | {"Cache_Misses", "", "50", "Service Time Percentiles 5min"}, 48 | {"Cache_Misses", "", "55", "Service Time Percentiles 5min"}, 49 | {"Cache_Misses", "", "60", "Service Time Percentiles 5min"}, 50 | {"Cache_Misses", "", "65", "Service Time Percentiles 5min"}, 51 | {"Cache_Misses", "", "70", "Service Time Percentiles 5min"}, 52 | {"Cache_Misses", "", "75", "Service Time Percentiles 5min"}, 53 | {"Cache_Misses", "", "80", "Service Time Percentiles 5min"}, 54 | {"Cache_Misses", "", "85", "Service Time Percentiles 5min"}, 55 | {"Cache_Misses", "", "90", "Service Time Percentiles 5min"}, 56 | {"Cache_Misses", "", "95", "Service Time Percentiles 5min"}, 57 | {"Cache_Hits", "", "5", "Service Time Percentiles 5min"}, 58 | {"Cache_Hits", "", "10", "Service Time Percentiles 5min"}, 59 | {"Cache_Hits", "", "15", "Service Time Percentiles 5min"}, 60 | {"Cache_Hits", "", "20", "Service Time Percentiles 5min"}, 61 | {"Cache_Hits", "", "25", "Service Time Percentiles 5min"}, 62 | {"Cache_Hits", "", "30", "Service Time Percentiles 5min"}, 63 | {"Cache_Hits", "", "35", "Service Time Percentiles 5min"}, 64 | {"Cache_Hits", "", "40", "Service Time Percentiles 5min"}, 65 | {"Cache_Hits", "", "45", "Service Time Percentiles 5min"}, 66 | {"Cache_Hits", "", "50", "Service Time Percentiles 5min"}, 67 | {"Cache_Hits", "", "55", "Service Time Percentiles 5min"}, 68 | {"Cache_Hits", "", "60", "Service Time Percentiles 5min"}, 69 | {"Cache_Hits", "", "65", "Service Time Percentiles 5min"}, 70 | {"Cache_Hits", "", "70", "Service Time Percentiles 5min"}, 71 | {"Cache_Hits", "", "75", "Service Time Percentiles 5min"}, 72 | {"Cache_Hits", "", "80", "Service Time Percentiles 5min"}, 73 | {"Cache_Hits", "", "85", "Service Time Percentiles 5min"}, 74 | {"Cache_Hits", "", "90", "Service Time Percentiles 5min"}, 75 | {"Cache_Hits", "", "95", "Service Time Percentiles 5min"}, 76 | {"Near_Hits", "", "5", "Service Time Percentiles 5min"}, 77 | {"Near_Hits", "", "10", "Service Time Percentiles 5min"}, 78 | {"Near_Hits", "", "15", "Service Time Percentiles 5min"}, 79 | {"Near_Hits", "", "20", "Service Time Percentiles 5min"}, 80 | {"Near_Hits", "", "25", "Service Time Percentiles 5min"}, 81 | {"Near_Hits", "", "30", "Service Time Percentiles 5min"}, 82 | {"Near_Hits", "", "35", "Service Time Percentiles 5min"}, 83 | {"Near_Hits", "", "40", "Service Time Percentiles 5min"}, 84 | {"Near_Hits", "", "45", "Service Time Percentiles 5min"}, 85 | {"Near_Hits", "", "50", "Service Time Percentiles 5min"}, 86 | {"Near_Hits", "", "55", "Service Time Percentiles 5min"}, 87 | {"Near_Hits", "", "60", "Service Time Percentiles 5min"}, 88 | {"Near_Hits", "", "65", "Service Time Percentiles 5min"}, 89 | {"Near_Hits", "", "70", "Service Time Percentiles 5min"}, 90 | {"Near_Hits", "", "75", "Service Time Percentiles 5min"}, 91 | {"Near_Hits", "", "80", "Service Time Percentiles 5min"}, 92 | {"Near_Hits", "", "85", "Service Time Percentiles 5min"}, 93 | {"Near_Hits", "", "90", "Service Time Percentiles 5min"}, 94 | {"Near_Hits", "", "95", "Service Time Percentiles 5min"}, 95 | {"DNS_Lookups", "", "5", "Service Time Percentiles 5min"}, 96 | {"DNS_Lookups", "", "10", "Service Time Percentiles 5min"}, 97 | {"DNS_Lookups", "", "15", "Service Time Percentiles 5min"}, 98 | {"DNS_Lookups", "", "20", "Service Time Percentiles 5min"}, 99 | {"DNS_Lookups", "", "25", "Service Time Percentiles 5min"}, 100 | {"DNS_Lookups", "", "30", "Service Time Percentiles 5min"}, 101 | {"DNS_Lookups", "", "35", "Service Time Percentiles 5min"}, 102 | {"DNS_Lookups", "", "40", "Service Time Percentiles 5min"}, 103 | {"DNS_Lookups", "", "45", "Service Time Percentiles 5min"}, 104 | {"DNS_Lookups", "", "50", "Service Time Percentiles 5min"}, 105 | {"DNS_Lookups", "", "55", "Service Time Percentiles 5min"}, 106 | {"DNS_Lookups", "", "60", "Service Time Percentiles 5min"}, 107 | {"DNS_Lookups", "", "65", "Service Time Percentiles 5min"}, 108 | {"DNS_Lookups", "", "70", "Service Time Percentiles 5min"}, 109 | {"DNS_Lookups", "", "75", "Service Time Percentiles 5min"}, 110 | {"DNS_Lookups", "", "80", "Service Time Percentiles 5min"}, 111 | {"DNS_Lookups", "", "85", "Service Time Percentiles 5min"}, 112 | {"DNS_Lookups", "", "90", "Service Time Percentiles 5min"}, 113 | {"DNS_Lookups", "", "95", "Service Time Percentiles 5min"}, 114 | } 115 | 116 | func generateSquidServiceTimes(labels []string) descMap { 117 | serviceTimes := descMap{} 118 | 119 | for i := range squidServiceTimess { 120 | serviceTime := squidServiceTimess[i] 121 | 122 | var key, name string 123 | 124 | if serviceTime.Counter != "" { 125 | key = fmt.Sprintf("%s_%s_%s", serviceTime.Section, serviceTime.Counter, serviceTime.Suffix) 126 | name = prometheus.BuildFQName(namespace, strings.Replace(serviceTime.Section, ".", "_", -1), 127 | fmt.Sprintf("%s_%s", serviceTime.Counter, serviceTime.Suffix)) 128 | } else { 129 | key = fmt.Sprintf("%s_%s", serviceTime.Section, serviceTime.Suffix) 130 | name = prometheus.BuildFQName(namespace, strings.Replace(serviceTime.Section, ".", "_", -1), 131 | serviceTime.Suffix) 132 | } 133 | 134 | serviceTimes[key] = prometheus.NewDesc( 135 | name, 136 | serviceTime.Description, 137 | labels, nil, 138 | ) 139 | } 140 | 141 | return serviceTimes 142 | } 143 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | defaultListenAddress = "127.0.0.1:9301" 15 | defaultWebConfigPath = "" 16 | defaultListenPort = 9301 17 | defaultMetricsPath = "/metrics" 18 | defaultSquidHostname = "localhost" 19 | defaultSquidPort = 3128 20 | defaultExtractServiceTimes = true 21 | defaultUseProxyHeader = false 22 | ) 23 | 24 | const ( 25 | squidExporterListenKey = "SQUID_EXPORTER_LISTEN" 26 | squidExporterWebConfigPathKey = "SQUID_EXPORTER_WEB_CONFIG_PATH" 27 | squidExporterMetricsPathKey = "SQUID_EXPORTER_METRICS_PATH" 28 | squidHostnameKey = "SQUID_HOSTNAME" 29 | squidPortKey = "SQUID_PORT" 30 | squidLoginKey = "SQUID_LOGIN" 31 | squidPasswordKey = "SQUID_PASSWORD" 32 | squidPidfile = "SQUID_PIDFILE" 33 | squidExtractServiceTimes = "SQUID_EXTRACTSERVICETIMES" 34 | squidUseProxyHeader = "SQUID_USE_PROXY_HEADER" 35 | ) 36 | 37 | var ( 38 | VersionFlag *bool 39 | ) 40 | 41 | type Labels struct { 42 | Keys []string 43 | Values []string 44 | } 45 | 46 | // Config configurations for exporter. 47 | type Config struct { 48 | ListenAddress string 49 | WebConfigPath string 50 | MetricPath string 51 | Labels Labels 52 | ExtractServiceTimes bool 53 | 54 | SquidHostname string 55 | SquidPort int 56 | Login string 57 | Password string 58 | Pidfile string 59 | 60 | UseProxyHeader bool 61 | } 62 | 63 | // NewConfig creates a new config object from command line args. 64 | func NewConfig() *Config { 65 | c := &Config{} 66 | 67 | flag.StringVar(&c.ListenAddress, "listen", 68 | loadEnvStringVar(squidExporterListenKey, defaultListenAddress), "Address and Port to bind exporter, in host:port format") 69 | flag.StringVar(&c.WebConfigPath, "web.config.file", loadEnvStringVar(squidExporterWebConfigPathKey, defaultWebConfigPath), 70 | "Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md") 71 | flag.StringVar(&c.MetricPath, "metrics-path", 72 | loadEnvStringVar(squidExporterMetricsPathKey, defaultMetricsPath), "Metrics path to expose prometheus metrics") 73 | 74 | flag.BoolVar(&c.ExtractServiceTimes, "extractservicetimes", 75 | loadEnvBoolVar(squidExtractServiceTimes, defaultExtractServiceTimes), "Extract service times metrics") 76 | 77 | flag.Var(&c.Labels, "label", "Custom metrics to attach to metrics, use -label multiple times for each additional label") 78 | 79 | flag.StringVar(&c.SquidHostname, "squid-hostname", 80 | loadEnvStringVar(squidHostnameKey, defaultSquidHostname), "Squid hostname") 81 | flag.IntVar(&c.SquidPort, "squid-port", 82 | loadEnvIntVar(squidPortKey, defaultSquidPort), "Squid port to read metrics") 83 | 84 | flag.StringVar(&c.Login, "squid-login", loadEnvStringVar(squidLoginKey, ""), "Login to squid service") 85 | flag.StringVar(&c.Password, "squid-password", loadEnvStringVar(squidPasswordKey, ""), "Password to squid service") 86 | 87 | flag.StringVar(&c.Pidfile, "squid-pidfile", loadEnvStringVar(squidPidfile, ""), "Optional path to the squid PID file for additional metrics") 88 | 89 | flag.BoolVar(&c.UseProxyHeader, "squid-use-proxy-header", 90 | loadEnvBoolVar(squidUseProxyHeader, defaultUseProxyHeader), "Use proxy headers when fetching metrics") 91 | 92 | VersionFlag = flag.Bool("version", false, "Print the version and exit") 93 | 94 | flag.Parse() 95 | 96 | return c 97 | } 98 | 99 | func loadEnvBoolVar(key string, def bool) bool { 100 | val := os.Getenv(key) 101 | if val == "" { 102 | return def 103 | } 104 | switch strings.ToLower(val) { 105 | case "true": 106 | return true 107 | case "false": 108 | return false 109 | default: 110 | return def 111 | } 112 | } 113 | 114 | func loadEnvStringVar(key, def string) string { 115 | val := os.Getenv(key) 116 | if val == "" { 117 | return def 118 | } 119 | 120 | return val 121 | } 122 | 123 | func loadEnvIntVar(key string, def int) int { 124 | valStr := os.Getenv(key) 125 | if valStr != "" { 126 | val, err := strconv.ParseInt(valStr, 0, 32) 127 | if err == nil { 128 | return int(val) 129 | } 130 | 131 | log.Printf("Error parsing %s='%s'. Integer value expected", key, valStr) 132 | } 133 | 134 | return def 135 | } 136 | 137 | func (l *Labels) String() string { 138 | var lbls []string 139 | for i := range l.Keys { 140 | lbls = append(lbls, l.Keys[i]+"="+l.Values[i]) 141 | } 142 | 143 | return strings.Join(lbls, ", ") 144 | } 145 | 146 | func (l *Labels) Set(value string) error { 147 | args := strings.Split(value, "=") 148 | 149 | if len(args) != 2 || len(args[1]) < 1 { 150 | return errors.New("label must be in 'key=value' format") 151 | } 152 | 153 | for _, key := range l.Keys { 154 | if key == args[0] { 155 | return fmt.Errorf("labels must be distinct; found duplicate key %q", args[0]) 156 | } 157 | } 158 | l.Keys = append(l.Keys, args[0]) 159 | l.Values = append(l.Values, args[1]) 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /dashboards/squid-sample-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [], 3 | "__requires": [ 4 | { 5 | "type": "grafana", 6 | "id": "grafana", 7 | "name": "Grafana", 8 | "version": "5.2.4" 9 | }, 10 | { 11 | "type": "panel", 12 | "id": "graph", 13 | "name": "Graph", 14 | "version": "5.0.0" 15 | }, 16 | { 17 | "type": "panel", 18 | "id": "singlestat", 19 | "name": "Singlestat", 20 | "version": "5.0.0" 21 | }, 22 | { 23 | "type": "panel", 24 | "id": "text", 25 | "name": "Text", 26 | "version": "5.0.0" 27 | } 28 | ], 29 | "annotations": { 30 | "list": [ 31 | { 32 | "builtIn": 1, 33 | "datasource": "-- Grafana --", 34 | "enable": true, 35 | "hide": true, 36 | "iconColor": "rgba(0, 211, 255, 1)", 37 | "name": "Annotations & Alerts", 38 | "type": "dashboard" 39 | } 40 | ] 41 | }, 42 | "editable": true, 43 | "gnetId": null, 44 | "graphTooltip": 0, 45 | "id": null, 46 | "iteration": 1536353270000, 47 | "links": [], 48 | "panels": [ 49 | { 50 | "content": "

Squid Sample Dashboard

", 51 | "gridPos": { 52 | "h": 3, 53 | "w": 6, 54 | "x": 0, 55 | "y": 0 56 | }, 57 | "id": 10, 58 | "links": [], 59 | "mode": "html", 60 | "title": "", 61 | "type": "text" 62 | }, 63 | { 64 | "cacheTimeout": null, 65 | "colorBackground": true, 66 | "colorValue": false, 67 | "colors": [ 68 | "#d44a3a", 69 | "rgba(237, 129, 40, 0.89)", 70 | "#299c46" 71 | ], 72 | "datasource": "$datasource", 73 | "format": "none", 74 | "gauge": { 75 | "maxValue": 100, 76 | "minValue": 0, 77 | "show": false, 78 | "thresholdLabels": false, 79 | "thresholdMarkers": true 80 | }, 81 | "gridPos": { 82 | "h": 3, 83 | "w": 6, 84 | "x": 6, 85 | "y": 0 86 | }, 87 | "id": 14, 88 | "interval": null, 89 | "links": [], 90 | "mappingType": 1, 91 | "mappingTypes": [ 92 | { 93 | "name": "value to text", 94 | "value": 1 95 | }, 96 | { 97 | "name": "range to text", 98 | "value": 2 99 | } 100 | ], 101 | "maxDataPoints": 100, 102 | "nullPointMode": "connected", 103 | "nullText": null, 104 | "postfix": "", 105 | "postfixFontSize": "200%", 106 | "prefix": "", 107 | "prefixFontSize": "200%", 108 | "rangeMaps": [ 109 | { 110 | "from": "null", 111 | "text": "N/A", 112 | "to": "null" 113 | } 114 | ], 115 | "sparkline": { 116 | "fillColor": "rgba(31, 118, 189, 0.18)", 117 | "full": false, 118 | "lineColor": "rgb(31, 120, 193)", 119 | "show": true 120 | }, 121 | "tableColumn": "", 122 | "targets": [ 123 | { 124 | "expr": "up{job='squid'}", 125 | "format": "time_series", 126 | "intervalFactor": 1, 127 | "legendFormat": "", 128 | "refId": "A" 129 | } 130 | ], 131 | "thresholds": "0,1", 132 | "title": "Services Up", 133 | "type": "singlestat", 134 | "valueFontSize": "200%", 135 | "valueMaps": [ 136 | { 137 | "op": "=", 138 | "text": "N/A", 139 | "value": "null" 140 | } 141 | ], 142 | "valueName": "current" 143 | }, 144 | { 145 | "cacheTimeout": null, 146 | "colorBackground": false, 147 | "colorValue": false, 148 | "colors": [ 149 | "#299c46", 150 | "rgba(237, 129, 40, 0.89)", 151 | "#d44a3a" 152 | ], 153 | "datasource": "$datasource", 154 | "format": "percentunit", 155 | "gauge": { 156 | "maxValue": 100, 157 | "minValue": 0, 158 | "show": false, 159 | "thresholdLabels": false, 160 | "thresholdMarkers": true 161 | }, 162 | "gridPos": { 163 | "h": 3, 164 | "w": 4, 165 | "x": 12, 166 | "y": 0 167 | }, 168 | "id": 8, 169 | "interval": null, 170 | "links": [], 171 | "mappingType": 1, 172 | "mappingTypes": [ 173 | { 174 | "name": "value to text", 175 | "value": 1 176 | }, 177 | { 178 | "name": "range to text", 179 | "value": 2 180 | } 181 | ], 182 | "maxDataPoints": 100, 183 | "nullPointMode": "connected", 184 | "nullText": null, 185 | "postfix": "", 186 | "postfixFontSize": "50%", 187 | "prefix": "", 188 | "prefixFontSize": "50%", 189 | "rangeMaps": [ 190 | { 191 | "from": "null", 192 | "text": "N/A", 193 | "to": "null" 194 | } 195 | ], 196 | "sparkline": { 197 | "fillColor": "rgba(31, 118, 189, 0.18)", 198 | "full": false, 199 | "lineColor": "rgb(31, 120, 193)", 200 | "show": false 201 | }, 202 | "tableColumn": "", 203 | "targets": [ 204 | { 205 | "expr": "squid_client_http_hit_kbytes_out_bytes_total / squid_client_http_kbytes_out_kbytes_total", 206 | "format": "time_series", 207 | "intervalFactor": 1, 208 | "refId": "A" 209 | } 210 | ], 211 | "thresholds": "", 212 | "title": "Bytes Hit Rate", 213 | "type": "singlestat", 214 | "valueFontSize": "80%", 215 | "valueMaps": [ 216 | { 217 | "op": "=", 218 | "text": "N/A", 219 | "value": "null" 220 | } 221 | ], 222 | "valueName": "avg" 223 | }, 224 | { 225 | "cacheTimeout": null, 226 | "colorBackground": false, 227 | "colorValue": false, 228 | "colors": [ 229 | "#299c46", 230 | "rgba(237, 129, 40, 0.89)", 231 | "#d44a3a" 232 | ], 233 | "datasource": "$datasource", 234 | "format": "percentunit", 235 | "gauge": { 236 | "maxValue": 100, 237 | "minValue": 0, 238 | "show": false, 239 | "thresholdLabels": false, 240 | "thresholdMarkers": true 241 | }, 242 | "gridPos": { 243 | "h": 3, 244 | "w": 4, 245 | "x": 16, 246 | "y": 0 247 | }, 248 | "id": 6, 249 | "interval": null, 250 | "links": [], 251 | "mappingType": 1, 252 | "mappingTypes": [ 253 | { 254 | "name": "value to text", 255 | "value": 1 256 | }, 257 | { 258 | "name": "range to text", 259 | "value": 2 260 | } 261 | ], 262 | "maxDataPoints": 100, 263 | "nullPointMode": "connected", 264 | "nullText": null, 265 | "postfix": "", 266 | "postfixFontSize": "50%", 267 | "prefix": "", 268 | "prefixFontSize": "50%", 269 | "rangeMaps": [ 270 | { 271 | "from": "null", 272 | "text": "N/A", 273 | "to": "null" 274 | } 275 | ], 276 | "sparkline": { 277 | "fillColor": "rgba(31, 118, 189, 0.18)", 278 | "full": false, 279 | "lineColor": "rgb(31, 120, 193)", 280 | "show": false 281 | }, 282 | "tableColumn": "", 283 | "targets": [ 284 | { 285 | "expr": "squid_client_http_hits_total / squid_client_http_requests_total", 286 | "format": "time_series", 287 | "intervalFactor": 1, 288 | "legendFormat": "Hit rate", 289 | "refId": "A" 290 | } 291 | ], 292 | "thresholds": "", 293 | "title": "Catch Hit Rate", 294 | "type": "singlestat", 295 | "valueFontSize": "80%", 296 | "valueMaps": [ 297 | { 298 | "op": "=", 299 | "text": "N/A", 300 | "value": "null" 301 | } 302 | ], 303 | "valueName": "avg" 304 | }, 305 | { 306 | "cacheTimeout": null, 307 | "colorBackground": false, 308 | "colorValue": false, 309 | "colors": [ 310 | "#299c46", 311 | "rgba(237, 129, 40, 0.89)", 312 | "#d44a3a" 313 | ], 314 | "datasource": "$datasource", 315 | "format": "percentunit", 316 | "gauge": { 317 | "maxValue": 100, 318 | "minValue": 0, 319 | "show": false, 320 | "thresholdLabels": false, 321 | "thresholdMarkers": true 322 | }, 323 | "gridPos": { 324 | "h": 3, 325 | "w": 4, 326 | "x": 20, 327 | "y": 0 328 | }, 329 | "id": 12, 330 | "interval": null, 331 | "links": [], 332 | "mappingType": 1, 333 | "mappingTypes": [ 334 | { 335 | "name": "value to text", 336 | "value": 1 337 | }, 338 | { 339 | "name": "range to text", 340 | "value": 2 341 | } 342 | ], 343 | "maxDataPoints": 100, 344 | "nullPointMode": "connected", 345 | "nullText": null, 346 | "postfix": "", 347 | "postfixFontSize": "50%", 348 | "prefix": "", 349 | "prefixFontSize": "50%", 350 | "rangeMaps": [ 351 | { 352 | "from": "null", 353 | "text": "N/A", 354 | "to": "null" 355 | } 356 | ], 357 | "sparkline": { 358 | "fillColor": "rgba(31, 118, 189, 0.18)", 359 | "full": false, 360 | "lineColor": "rgb(31, 120, 193)", 361 | "show": false 362 | }, 363 | "tableColumn": "", 364 | "targets": [ 365 | { 366 | "expr": "squid_client_http_errors_total / squid_client_http_requests_total", 367 | "format": "time_series", 368 | "intervalFactor": 1, 369 | "refId": "A" 370 | } 371 | ], 372 | "thresholds": "", 373 | "title": "Error Rate", 374 | "type": "singlestat", 375 | "valueFontSize": "80%", 376 | "valueMaps": [ 377 | { 378 | "op": "=", 379 | "text": "N/A", 380 | "value": "null" 381 | } 382 | ], 383 | "valueName": "avg" 384 | }, 385 | { 386 | "aliasColors": {}, 387 | "bars": false, 388 | "dashLength": 10, 389 | "dashes": false, 390 | "datasource": "$datasource", 391 | "fill": 1, 392 | "gridPos": { 393 | "h": 6, 394 | "w": 12, 395 | "x": 0, 396 | "y": 3 397 | }, 398 | "id": 2, 399 | "legend": { 400 | "avg": false, 401 | "current": false, 402 | "max": false, 403 | "min": false, 404 | "show": true, 405 | "total": false, 406 | "values": false 407 | }, 408 | "lines": true, 409 | "linewidth": 1, 410 | "links": [], 411 | "nullPointMode": "null", 412 | "percentage": false, 413 | "pointradius": 5, 414 | "points": false, 415 | "renderer": "flot", 416 | "seriesOverrides": [], 417 | "spaceLength": 10, 418 | "stack": false, 419 | "steppedLine": false, 420 | "targets": [ 421 | { 422 | "expr": "rate(squid_client_http_requests_total[5m])", 423 | "format": "time_series", 424 | "interval": "", 425 | "intervalFactor": 1, 426 | "legendFormat": "Total Requests", 427 | "refId": "B" 428 | }, 429 | { 430 | "expr": "rate(squid_client_http_hits_total[5m])", 431 | "format": "time_series", 432 | "interval": "", 433 | "intervalFactor": 1, 434 | "legendFormat": "Total HTTP hits", 435 | "refId": "A" 436 | }, 437 | { 438 | "expr": "rate(squid_client_http_errors_total[5m])", 439 | "format": "time_series", 440 | "intervalFactor": 1, 441 | "legendFormat": "Total Errors", 442 | "refId": "C" 443 | } 444 | ], 445 | "thresholds": [], 446 | "timeFrom": null, 447 | "timeShift": null, 448 | "title": "Client Requests", 449 | "tooltip": { 450 | "shared": true, 451 | "sort": 0, 452 | "value_type": "individual" 453 | }, 454 | "type": "graph", 455 | "xaxis": { 456 | "buckets": null, 457 | "mode": "time", 458 | "name": null, 459 | "show": true, 460 | "values": [] 461 | }, 462 | "yaxes": [ 463 | { 464 | "format": "reqps", 465 | "label": null, 466 | "logBase": 1, 467 | "max": null, 468 | "min": null, 469 | "show": true 470 | }, 471 | { 472 | "format": "short", 473 | "label": null, 474 | "logBase": 1, 475 | "max": null, 476 | "min": null, 477 | "show": true 478 | } 479 | ], 480 | "yaxis": { 481 | "align": false, 482 | "alignLevel": null 483 | } 484 | }, 485 | { 486 | "aliasColors": {}, 487 | "bars": false, 488 | "dashLength": 10, 489 | "dashes": false, 490 | "datasource": "$datasource", 491 | "fill": 1, 492 | "gridPos": { 493 | "h": 6, 494 | "w": 12, 495 | "x": 12, 496 | "y": 3 497 | }, 498 | "id": 4, 499 | "legend": { 500 | "avg": false, 501 | "current": false, 502 | "max": false, 503 | "min": false, 504 | "show": true, 505 | "total": false, 506 | "values": false 507 | }, 508 | "lines": true, 509 | "linewidth": 1, 510 | "links": [], 511 | "nullPointMode": "null", 512 | "percentage": false, 513 | "pointradius": 5, 514 | "points": false, 515 | "renderer": "flot", 516 | "seriesOverrides": [], 517 | "spaceLength": 10, 518 | "stack": false, 519 | "steppedLine": false, 520 | "targets": [ 521 | { 522 | "expr": "rate(squid_client_http_kbytes_in_kbytes_total[5m]) * -1", 523 | "format": "time_series", 524 | "interval": "", 525 | "intervalFactor": 1, 526 | "legendFormat": "HTTP Traffic in", 527 | "refId": "A" 528 | }, 529 | { 530 | "expr": "rate(squid_client_http_kbytes_out_kbytes_total[5m])", 531 | "format": "time_series", 532 | "intervalFactor": 1, 533 | "legendFormat": "HTTP Traffic Out", 534 | "refId": "B" 535 | }, 536 | { 537 | "expr": "rate(squid_client_http_hit_kbytes_out_bytes_total[5m])", 538 | "format": "time_series", 539 | "interval": "", 540 | "intervalFactor": 1, 541 | "legendFormat": "HTTP Traffic Out Hits", 542 | "refId": "C" 543 | } 544 | ], 545 | "thresholds": [], 546 | "timeFrom": null, 547 | "timeShift": null, 548 | "title": "Client Traffic", 549 | "tooltip": { 550 | "shared": true, 551 | "sort": 0, 552 | "value_type": "individual" 553 | }, 554 | "type": "graph", 555 | "xaxis": { 556 | "buckets": null, 557 | "mode": "time", 558 | "name": null, 559 | "show": true, 560 | "values": [] 561 | }, 562 | "yaxes": [ 563 | { 564 | "format": "KBs", 565 | "label": null, 566 | "logBase": 1, 567 | "max": null, 568 | "min": null, 569 | "show": true 570 | }, 571 | { 572 | "format": "short", 573 | "label": null, 574 | "logBase": 1, 575 | "max": null, 576 | "min": null, 577 | "show": true 578 | } 579 | ], 580 | "yaxis": { 581 | "align": false, 582 | "alignLevel": null 583 | } 584 | } 585 | ], 586 | "schemaVersion": 16, 587 | "style": "dark", 588 | "tags": [], 589 | "templating": { 590 | "list": [ 591 | { 592 | "current": { 593 | "text": "default", 594 | "value": "default" 595 | }, 596 | "hide": 0, 597 | "label": null, 598 | "name": "datasource", 599 | "options": [], 600 | "query": "prometheus", 601 | "refresh": 1, 602 | "regex": "", 603 | "type": "datasource" 604 | } 605 | ] 606 | }, 607 | "time": { 608 | "from": "now-6h", 609 | "to": "now" 610 | }, 611 | "timepicker": { 612 | "refresh_intervals": [ 613 | "5s", 614 | "10s", 615 | "30s", 616 | "1m", 617 | "5m", 618 | "15m", 619 | "30m", 620 | "1h", 621 | "2h", 622 | "1d" 623 | ], 624 | "time_options": [ 625 | "5m", 626 | "15m", 627 | "1h", 628 | "6h", 629 | "12h", 630 | "24h", 631 | "2d", 632 | "7d", 633 | "30d" 634 | ] 635 | }, 636 | "timezone": "", 637 | "title": "Squid Sample Dashboard", 638 | "uid": "yRc_Bj2ik", 639 | "version": 3 640 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Travis Docker deploy script 4 | IMAGE_NAME="boynux/squid-exporter" 5 | IMAGE_TAG=${TRAVIS_TAG:-latest} 6 | 7 | docker --version 8 | docker build -t $IMAGE_NAME:$IMAGE_TAG . 9 | docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest 10 | echo $DOCKER_API_KEY | docker login -u boynux --password-stdin 11 | docker push $IMAGE_NAME:$IMAGE_TAG 12 | docker push $IMAGE_NAME:latest 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/boynux/squid-exporter 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-kit/log v0.2.1 7 | github.com/pires/go-proxyproto v0.6.2 8 | github.com/prometheus/client_golang v1.19.0 9 | github.com/prometheus/common v0.53.0 10 | github.com/prometheus/exporter-toolkit v0.11.0 11 | github.com/stretchr/testify v1.7.0 12 | ) 13 | 14 | require ( 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 17 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/go-logfmt/logfmt v0.5.1 // indirect 20 | github.com/golang/protobuf v1.5.3 // indirect 21 | github.com/jpillora/backoff v1.0.0 // indirect 22 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/prometheus/client_model v0.6.0 // indirect 25 | github.com/prometheus/procfs v0.12.0 // indirect 26 | golang.org/x/crypto v0.21.0 // indirect 27 | golang.org/x/net v0.22.0 // indirect 28 | golang.org/x/oauth2 v0.18.0 // indirect 29 | golang.org/x/sync v0.5.0 // indirect 30 | golang.org/x/sys v0.18.0 // indirect 31 | golang.org/x/text v0.14.0 // indirect 32 | google.golang.org/appengine v1.6.7 // indirect 33 | google.golang.org/protobuf v1.33.0 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 6 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 11 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 12 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 13 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 14 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 15 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 16 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 17 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 18 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 19 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 22 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 23 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 24 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 25 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 26 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 27 | github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= 28 | github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 32 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 33 | github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= 34 | github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= 35 | github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= 36 | github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= 37 | github.com/prometheus/exporter-toolkit v0.11.0 h1:yNTsuZ0aNCNFQ3aFTD2uhPOvr4iD7fdBvKPAEGkNf+g= 38 | github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q= 39 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 40 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 41 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 44 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 46 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 47 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 48 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 49 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 50 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 51 | golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= 52 | golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 53 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 54 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 55 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 57 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 60 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 61 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 62 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 63 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 64 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 65 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 66 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 67 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 68 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 69 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 72 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 73 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 74 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 75 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: squid-exporter 3 | description: A helm chart to deploy Squid Exporter. 4 | type: application 5 | version: 0.1.0 6 | appVersion: "v1.10.3" 7 | -------------------------------------------------------------------------------- /helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "squid-exporter.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "squid-exporter.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "squid-exporter.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "squid-exporter.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "squid-exporter.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "squid-exporter.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "squid-exporter.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "squid-exporter.labels" -}} 37 | helm.sh/chart: {{ include "squid-exporter.chart" . }} 38 | {{ include "squid-exporter.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | squid-exporter.boynux.com/proxy-hostname: {{ .Values.squidConfig.hostname }} 44 | squid-exporter.boynux.com/proxy-port: {{ .Values.squidConfig.port | quote }} 45 | {{- end }} 46 | 47 | {{/* 48 | Selector labels 49 | */}} 50 | {{- define "squid-exporter.selectorLabels" -}} 51 | app.kubernetes.io/name: {{ include "squid-exporter.name" . }} 52 | app.kubernetes.io/instance: {{ .Release.Name }} 53 | {{- end }} 54 | 55 | {{/* 56 | Create the name of the service account to use 57 | */}} 58 | {{- define "squid-exporter.serviceAccountName" -}} 59 | {{- if .Values.serviceAccount.create }} 60 | {{- default (include "squid-exporter.fullname" .) .Values.serviceAccount.name }} 61 | {{- else }} 62 | {{- default "default" .Values.serviceAccount.name }} 63 | {{- end }} 64 | {{- end }} 65 | -------------------------------------------------------------------------------- /helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "squid-exporter.fullname" . }} 5 | labels: 6 | {{- include "squid-exporter.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | {{- include "squid-exporter.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | annotations: 15 | checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} 16 | {{- with .Values.podAnnotations }} 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | labels: 20 | {{- include "squid-exporter.selectorLabels" . | nindent 8 }} 21 | spec: 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | serviceAccountName: {{ include "squid-exporter.serviceAccountName" . }} 27 | securityContext: 28 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 29 | containers: 30 | - name: {{ .Chart.Name }} 31 | securityContext: 32 | {{- toYaml .Values.securityContext | nindent 12 }} 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy }} 35 | envFrom: 36 | - secretRef: 37 | name: {{ include "squid-exporter.fullname" . }} 38 | env: 39 | - name: SQUID_EXPORTER_LISTEN 40 | value: "0.0.0.0:9301" 41 | - name: SQUID_EXPORTER_METRICS_PATH 42 | value: /metrics 43 | - name: SQUID_HOSTNAME 44 | value: {{ required ".Values.squidConfig.hostname must be defined" .Values.squidConfig.hostname }} 45 | - name: SQUID_PORT 46 | value: {{ .Values.squidConfig.port | quote }} 47 | ports: 48 | - name: metrics 49 | containerPort: 9301 50 | protocol: TCP 51 | livenessProbe: 52 | httpGet: 53 | path: / 54 | port: metrics 55 | readinessProbe: 56 | httpGet: 57 | path: / 58 | port: metrics 59 | resources: 60 | {{- toYaml .Values.resources | nindent 12 }} 61 | {{- with .Values.nodeSelector }} 62 | nodeSelector: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | {{- with .Values.affinity }} 66 | affinity: 67 | {{- toYaml . | nindent 8 }} 68 | {{- end }} 69 | {{- with .Values.tolerations }} 70 | tolerations: 71 | {{- toYaml . | nindent 8 }} 72 | {{- end }} 73 | -------------------------------------------------------------------------------- /helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "squid-exporter.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "squid-exporter.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /helm/templates/prometheusrules.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.rules }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: PrometheusRule 4 | metadata: 5 | name: {{ include "squid-exporter.fullname" . }} 6 | labels: 7 | {{- include "squid-exporter.labels" . | nindent 4 }} 8 | {{- with .Values.serviceMonitor.labels }} 9 | {{- . | toYaml | nindent 4 }} 10 | {{- end }} 11 | spec: 12 | groups: 13 | - name: {{ include "squid-exporter.fullname" . }} 14 | rules: 15 | {{- .Values.serviceMonitor.rules | toYaml | nindent 8 }} 16 | {{- end }} -------------------------------------------------------------------------------- /helm/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "squid-exporter.fullname" . }} 5 | labels: 6 | {{- include "squid-exporter.labels" . | nindent 4 }} 7 | data: 8 | SQUID_LOGIN: {{ .Values.squidConfig.login | b64enc }} 9 | SQUID_PASSWORD: {{ .Values.squidConfig.password | b64enc }} -------------------------------------------------------------------------------- /helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "squid-exporter.fullname" . }} 5 | labels: 6 | {{- include "squid-exporter.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | {{- if or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer") }} 10 | externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} 11 | {{- end }} 12 | sessionAffinity: {{ .Values.service.sessionAffinity }} 13 | ports: 14 | - port: {{ .Values.service.port }} 15 | targetPort: metrics 16 | protocol: TCP 17 | name: metrics 18 | {{- if or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer") }} 19 | nodePort: {{ .Values.service.nodePort }} 20 | {{- end }} 21 | selector: 22 | {{- include "squid-exporter.selectorLabels" . | nindent 4 }} 23 | -------------------------------------------------------------------------------- /helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "squid-exporter.serviceAccountName" . }} 6 | labels: 7 | {{- include "squid-exporter.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /helm/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: {{ include "squid-exporter.fullname" . }} 5 | labels: 6 | {{- include "squid-exporter.labels" . | nindent 4 }} 7 | {{- with .Values.serviceMonitor.labels }} 8 | {{- . | toYaml | nindent 4 }} 9 | {{- end }} 10 | spec: 11 | endpoints: 12 | - port: metrics 13 | interval: {{ .Values.serviceMonitor.interval }} 14 | metricRelabelings: 15 | {{- .Values.serviceMonitor.additionalMetricsRelabels | toYaml | nindent 10 }} 16 | path: "/metrics" 17 | relabelings: 18 | {{- .Values.serviceMonitor.additionalRelabeling | toYaml | nindent 10 }} 19 | scheme: http 20 | scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} 21 | targetLabels: 22 | {{- range $label, $val := .Values.serviceMonitor.labels }} 23 | - {{ $label }} 24 | {{- end }} 25 | - squid-exporter.boynux.com/proxy-hostname 26 | - squid-exporter.boynux.com/proxy-port 27 | selector: 28 | matchLabels: 29 | {{- include "squid-exporter.selectorLabels" . | nindent 8 }} -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for squid-exporter. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | squidConfig: 6 | login: "" 7 | password: "" 8 | hostname: "" 9 | port: 3128 10 | 11 | # Under normal circumstances one replica is needed. 12 | replicaCount: 1 13 | 14 | image: 15 | repository: boynux/squid-exporter 16 | pullPolicy: IfNotPresent 17 | # Overrides the image tag whose default is the chart appVersion. 18 | tag: "" 19 | 20 | imagePullSecrets: [] 21 | nameOverride: "" 22 | fullnameOverride: "" 23 | 24 | serviceAccount: 25 | # Specifies whether a service account should be created 26 | create: true 27 | # Annotations to add to the service account 28 | annotations: {} 29 | # The name of the service account to use. 30 | # If not set and create is true, a name is generated using the fullname template 31 | name: "" 32 | 33 | podAnnotations: {} 34 | 35 | podSecurityContext: 36 | {} 37 | # fsGroup: 2000 38 | 39 | securityContext: 40 | {} 41 | # capabilities: 42 | # drop: 43 | # - ALL 44 | # readOnlyRootFilesystem: true 45 | # runAsNonRoot: true 46 | # runAsUser: 1000 47 | 48 | service: 49 | # Kubernetes Service type, one of [ClusterIP, NodePort, LoadBalancer] 50 | type: ClusterIP 51 | port: 80 52 | 53 | # Supports either ClientIP or None. 54 | # Used to maintain session affinity. 55 | # Enable client IP based session affinity. 56 | # https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies 57 | sessionAffinity: ClientIP 58 | 59 | # Service settings below are applicable only if 60 | # service.type is one of [LoadBalancer, NodePort] and not ClusterIP. 61 | # Possible values are [Cluster, Local]. 62 | # If set to Local, then the Service's port will be available only on Kubernetes 63 | # Nodes which have the Squid Pods so no Kubernetes Node-to-Node traffic will be forwarded. 64 | # If set to Cluster, then the Service's port will be available on any Node of 65 | # a Kubernetes cluster. The drawback is that Kubernetes will use double NAT 66 | # so it will hide the Client source IP from Squid. 67 | externalTrafficPolicy: Cluster 68 | # Node port to listen on. Typically, Kubernetes allows ports in range 30000-32767 69 | # see https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport 70 | # for more information. 71 | nodePort: "" 72 | 73 | ingress: 74 | enabled: false 75 | className: "" 76 | annotations: 77 | {} 78 | # kubernetes.io/ingress.class: nginx 79 | # kubernetes.io/tls-acme: "true" 80 | hosts: 81 | - host: chart-example.local 82 | paths: 83 | - path: / 84 | pathType: ImplementationSpecific 85 | tls: [] 86 | # - secretName: chart-example-tls 87 | # hosts: 88 | # - chart-example.local 89 | 90 | resources: 91 | {} 92 | # We usually recommend not to specify default resources and to leave this as a conscious 93 | # choice for the user. This also increases chances charts run on environments with little 94 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 95 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 96 | # limits: 97 | # cpu: 100m 98 | # memory: 128Mi 99 | # requests: 100 | # cpu: 100m 101 | # memory: 128Mi 102 | 103 | nodeSelector: {} 104 | 105 | tolerations: [] 106 | 107 | affinity: {} 108 | 109 | serviceMonitor: 110 | enabled: false 111 | additionalMetricsRelabels: [] 112 | additionalRelabeling: [] 113 | labels: {} 114 | interval: 30s 115 | scrapeTimeout: 30s 116 | # Prometheus Operator rules to install 117 | rules: 118 | [] 119 | # - alert: SquidDown 120 | # annotations: 121 | # message: Exporter can not collect metrics from Squid proxy server {{ $labels.host }} 122 | # expr: squid_up == 0 123 | # for: 5m 124 | # labels: 125 | # severity: critical 126 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "strconv" 7 | "strings" 8 | 9 | proxyproto "github.com/pires/go-proxyproto" 10 | 11 | "github.com/boynux/squid-exporter/config" 12 | ) 13 | 14 | func createProxyHeader(cfg *config.Config) string { 15 | la := strings.Split(cfg.ListenAddress, ":") 16 | if len(la) < 2 { 17 | log.Printf("Cannot parse listen address (%s). Failed to create proxy header\n", cfg.ListenAddress) 18 | return "" 19 | } 20 | 21 | spt, err := strconv.Atoi(la[1]) 22 | if err != nil { 23 | log.Printf("Failed to create proxy header: %v\n", err.Error()) 24 | return "" 25 | } 26 | 27 | sip, err := net.LookupIP(la[0]) 28 | if err != nil { 29 | log.Printf("Failed to create proxy header: %v\n", err.Error()) 30 | return "" 31 | } 32 | 33 | dip, err := net.LookupIP(cfg.SquidHostname) 34 | if err != nil { 35 | log.Printf("Failed to create proxy header: %v\n", err.Error()) 36 | return "" 37 | } 38 | 39 | ph := &proxyproto.Header{ 40 | Version: 1, 41 | Command: proxyproto.PROXY, 42 | TransportProtocol: proxyproto.TCPv4, 43 | SourceAddr: &net.TCPAddr{ 44 | IP: sip[0], 45 | Port: spt, 46 | }, 47 | 48 | DestinationAddr: &net.TCPAddr{ 49 | IP: dip[0], 50 | Port: cfg.SquidPort, 51 | }, 52 | } 53 | phs, err := ph.Format() 54 | 55 | if err != nil { 56 | log.Printf("Failed to create proxy header: %v\n", err.Error()) 57 | } 58 | 59 | // proxyproto adds crlf to the end of the header string, but we will add this later 60 | // we are triming it here. 61 | return strings.TrimSuffix(string(phs), "\r\n") 62 | } 63 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/boynux/squid-exporter/config" 9 | ) 10 | 11 | func TestCreatProxyHelper(t *testing.T) { 12 | cfg := &config.Config{ 13 | ListenAddress: "192.0.2.1:3192", 14 | SquidHostname: "127.0.0.1", 15 | SquidPort: 3128, 16 | } 17 | 18 | expectedHProxyString := "PROXY TCP4 192.0.2.1 127.0.0.1 3192 3128" 19 | 20 | p := createProxyHeader(cfg) 21 | assert.Equal(t, expectedHProxyString, p, "Proxy headers do not match!") 22 | } 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | kitlog "github.com/go-kit/log" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/collectors" 14 | versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | "github.com/prometheus/common/version" 17 | "github.com/prometheus/exporter-toolkit/web" 18 | 19 | "github.com/boynux/squid-exporter/collector" 20 | "github.com/boynux/squid-exporter/config" 21 | ) 22 | 23 | func init() { 24 | prometheus.MustRegister(versioncollector.NewCollector("squid_exporter")) 25 | } 26 | 27 | func main() { 28 | cfg := config.NewConfig() 29 | if *config.VersionFlag { 30 | log.Println(version.Print("squid_exporter")) 31 | os.Exit(0) 32 | } 33 | collector.ExtractServiceTimes = cfg.ExtractServiceTimes 34 | 35 | proxyHeader := "" 36 | 37 | if cfg.UseProxyHeader { 38 | proxyHeader = createProxyHeader(cfg) 39 | } 40 | 41 | log.Println("Scraping metrics from", fmt.Sprintf("%s:%d", cfg.SquidHostname, cfg.SquidPort)) 42 | e := collector.New(&collector.CollectorConfig{ 43 | Hostname: cfg.SquidHostname, 44 | Port: cfg.SquidPort, 45 | Login: cfg.Login, 46 | Password: cfg.Password, 47 | Labels: cfg.Labels, 48 | ProxyHeader: proxyHeader, 49 | }) 50 | prometheus.MustRegister(e) 51 | 52 | if cfg.Pidfile != "" { 53 | procExporter := collectors.NewProcessCollector(collectors.ProcessCollectorOpts{ 54 | PidFn: func() (int, error) { 55 | content, err := os.ReadFile(cfg.Pidfile) 56 | if err != nil { 57 | return 0, fmt.Errorf("cannot read pid file %q: %w", cfg.Pidfile, err) 58 | } 59 | value, err := strconv.Atoi(strings.TrimSpace(string(content))) 60 | if err != nil { 61 | return 0, fmt.Errorf("cannot parse pid file %q: %w", cfg.Pidfile, err) 62 | } 63 | return value, nil 64 | }, 65 | Namespace: "squid", 66 | }) 67 | prometheus.MustRegister(procExporter) 68 | } 69 | 70 | // Serve metrics 71 | http.Handle(cfg.MetricPath, promhttp.Handler()) 72 | 73 | if cfg.MetricPath != "/" { 74 | landingConfig := web.LandingConfig{ 75 | Name: "Squid Exporter", 76 | Description: "Prometheus exporter for Squid caching proxy servers", 77 | HeaderColor: "#15a5be", 78 | Version: version.Info(), 79 | Links: []web.LandingLinks{ 80 | { 81 | Address: cfg.MetricPath, 82 | Text: "Metrics", 83 | }, 84 | }, 85 | } 86 | landingPage, err := web.NewLandingPage(landingConfig) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | http.Handle("/", landingPage) 91 | } 92 | 93 | systemdSocket := false 94 | toolkitFlags := &web.FlagConfig{ 95 | WebListenAddresses: &[]string{cfg.ListenAddress}, 96 | WebSystemdSocket: &systemdSocket, 97 | WebConfigFile: &cfg.WebConfigPath, 98 | } 99 | logger := kitlog.NewLogfmtLogger(kitlog.StdlibWriter{}) 100 | 101 | server := &http.Server{} 102 | log.Fatal(web.ListenAndServe(server, toolkitFlags, logger)) 103 | } 104 | -------------------------------------------------------------------------------- /prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | # The job name is added as a label `job=` to any timeseries scraped from this config. 23 | - job_name: 'squid' 24 | 25 | # metrics_path defaults to '/metrics' 26 | # scheme defaults to 'http' 27 | 28 | static_configs: 29 | - targets: ['localhost:9301'] 30 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // VarLabel maps key value Prometheus labels. 4 | type VarLabel struct { 5 | Key string 6 | Value string 7 | } 8 | 9 | // Counter maps a Squid counter. 10 | type Counter struct { 11 | Key string 12 | Value float64 13 | VarLabels []VarLabel 14 | } 15 | 16 | // Counters is a list of multiple Squid counters. 17 | type Counters []Counter 18 | --------------------------------------------------------------------------------