pax_global_header 0000666 0000000 0000000 00000000064 13605621457 0014523 g ustar 00root root 0000000 0000000 52 comment=1cbf5e536d6275e21df39de8d31c4f0ec90b0b99 prometheus-squid-exporter-1.8.2+ds/ 0000775 0000000 0000000 00000000000 13605621457 0017321 5 ustar 00root root 0000000 0000000 prometheus-squid-exporter-1.8.2+ds/.github/ 0000775 0000000 0000000 00000000000 13605621457 0020661 5 ustar 00root root 0000000 0000000 prometheus-squid-exporter-1.8.2+ds/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 13605621457 0023044 5 ustar 00root root 0000000 0000000 prometheus-squid-exporter-1.8.2+ds/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000000703 13605621457 0025536 0 ustar 00root root 0000000 0000000 --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. **OS (please complete the following information):** - OS: [e.g. Ubuntu] - Version [e.g. v0.4] **Additional context** Add any other context about the problem here. prometheus-squid-exporter-1.8.2+ds/.gitignore 0000664 0000000 0000000 00000000025 13605621457 0021306 0 ustar 00root root 0000000 0000000 bin/* squid-exporter prometheus-squid-exporter-1.8.2+ds/.travis.yml 0000664 0000000 0000000 00000001467 13605621457 0021442 0 ustar 00root root 0000000 0000000 language: go deploy: provider: releases api_key: secure: "iumDvYQyMvTdD2AHxFDIVpixE7GK5VhCDo4wqkH9s3lmLIXTw2LPgUq4tx73aV7PGKskvWpILq87bh6t1AlFYnk/0VF12zMQWujsvh/iSdyCVvraobhYjiwQyHkFSoTBZ0xpwVImqRhF/AcAP/zwCTAGZMZMSm8xZucqkPaj02o8xgCKgPyDwv/IPo2Iiy6LmLbYeWBFScMtEyPqxjoKL5lL3b6e1W0esbJvPhzWQvTn2IBB8Ntm5hBBcqzG+/K3dNLpRTefX1xJlo2c5ZFT+KPFqIvNviNne7OhlX+NGZ33jUXVDPF4Lk6jLr49QnlTbqz8CKp3QzlpzOs+eF+2BBl5Gc1vJ0thmZ/TEujYD0rGzwDQ6nCVC6/VBbPiYhmQigk+AYFfxgutzHIfnuuySEoi5KOLNINtppwUTYjuxUv+YkQTUswCTa2MBoL0ZfAkdyrbBkGRL78sPRDRLK6KFXqz0Bk+pQdS0PebNclqaPnEML1jQYdRBgBQ0uFGcfOLkmIDr/L7icSif4uDrwaE1GnhHeloUOlaTSc63BSw1GdC+iYiGi8/TNg0Nw5co+M6Ii/Wuea9ngayAhVgNC64hV1giQEPPTfuS3EAxBf4ytybuqH3p+VtqD3ES97OqGGup4PHHQVPEdc6bzsD2aThLCeqpS/O9By9PiQcku+L+oo=" file: bin/squid-exporter skip_cleanup: true on: tags: true prometheus-squid-exporter-1.8.2+ds/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006215 13605621457 0022124 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct ## Our Pledge 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. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities 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. 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. ## Scope 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. ## Enforcement 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. 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. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ prometheus-squid-exporter-1.8.2+ds/Dockerfile 0000664 0000000 0000000 00000000746 13605621457 0021322 0 ustar 00root root 0000000 0000000 FROM golang:alpine as builder WORKDIR /go/src/github.com/boynux/squid-exporter COPY . . # Compile the binary statically, so it can be run without libraries. RUN CGO_ENABLED=0 GOOS=linux go install -a -ldflags '-extldflags "-s -w -static"' . FROM scratch COPY --from=builder /go/bin/squid-exporter /usr/local/bin/squid-exporter # Allow /etc/hosts to be used for DNS COPY --from=builder /etc/nsswitch.conf /etc/nsswitch.conf EXPOSE 9301 ENTRYPOINT ["/usr/local/bin/squid-exporter"] prometheus-squid-exporter-1.8.2+ds/LICENSE 0000664 0000000 0000000 00000002056 13605621457 0020331 0 ustar 00root root 0000000 0000000 MIT License Copyright (c) 2017 Mohammad Arab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. prometheus-squid-exporter-1.8.2+ds/Makefile 0000664 0000000 0000000 00000001407 13605621457 0020763 0 ustar 00root root 0000000 0000000 .PHONY: all clean all: test build EXE = ./bin/squid-exporter SRC = $(shell find ./ -type f -name '*.go') VERSION ?= $(shell cat VERSION) REVISION = $(shell git rev-parse HEAD) BRANCH = $(shell git rev-parse --abbrev-ref HEAD) LDFLAGS = -extldflags "-s -w -static" \ -X github.com/boynux/squid-exporter/vendor/github.com/prometheus/common/version.Version=$(VERSION) \ -X github.com/boynux/squid-exporter/vendor/github.com/prometheus/common/version.Revision=$(REVISION) \ -X github.com/boynux/squid-exporter/vendor/github.com/prometheus/common/version.Branch=$(BRANCH) $(EXE): $(SRC) CGO_ENABLED=0 GOOS=linux go build -a -ldflags '$(LDFLAGS)' -o $(EXE) . test: go test -v ./... build: $(EXE) docker: docker build -t squid-exporter . clean: rm -f $(EXE) prometheus-squid-exporter-1.8.2+ds/README.md 0000664 0000000 0000000 00000006640 13605621457 0020606 0 ustar 00root root 0000000 0000000 [](https://travis-ci.org/boynux/squid-exporter) [](https://goreportcard.com/report/github.com/boynux/squid-exporter) [](https://codeclimate.com/github/boynux/squid-exporter) Squid Prometheus exporter -------------------------- Exports squid metrics in Prometheus format **NOTE**: From release 1.0 metric names and some parameters has changed. Make sure you check the docs and update your deployments accordingly! New ----- * Using environment variables to configure the exporter * Adding custom labels to metrics Usage: ------ Simple usage: squid-exporter -squid-hostname "localhost" -squid-port 3128 [Configure Prometheus](https://github.com/boynux/squid-exporter/blob/master/prometheus/prometheus.yml) to scrape metrics from `localhost:9301/metrics` - job_name: squid # squid-exporter is installed, grab stats about the local # squid instance. target_groups: - targets: ['localhost:9301'] To get all the parameteres, command line arguments always override default and environment variables configs: squid-exporter -help The following environment variables can be used to override default parameters: ``` SQUID_EXPORTER_LISTEN SQUID_EXPORTER_METRICS_PATH SQUID_HOSTNAME SQUID_PORT SQUID_LOGIN SQUID_PASSWORD ``` Usage with docker: ------ Basic setup assuming Squid is running on the same machine: docker run --net=host -d boynux/squid-exporter Setup with Squid running on a different host docker run -p 9301:9301 -d boynux/squid-exporter -squid-hostname "192.168.0.2" -squid-port 3128 -listen ":9301" With environment variables 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 Build: -------- This project is written in Go, so all the usual methods for building (or cross compiling) a Go application would work. If you are not very familiar with Go you can download the binary from [releases](https://github.com/boynux/squid-exporter/releases). Or build it for your OS: `go install https://github.com/boynux/squid-exporter` then you can find the binary in: `$GOPATH/bin/squid-exporter` Features: --------- - [ ] Expose Squid counters - [x] Client HTTP - [x] Server HTTP - [x] Server ALL - [x] Server FTP - [x] Server Other - [ ] ICP - [ ] CD - [x] Swap - [ ] Page Faults - [ ] Others - [ ] Histograms - [ ] Other metrics - [x] Squid Authentication (Basic Auth) FAQ: -------- - Q: Metrics are not reported by exporter - A: That usually means the exporter cannot reach squid server or the config manager permissions are not set corretly. To debug and mitigate: - First make sure the exporter service can reach to squid server IP Address (you can use telnet to test that) - Make sure you allow exporter to query the squid server in config you will need something like this (`172.20.0.0/16` is the network for exporter, you can also use a single IP if needed): ``` #http_access allow manager localhost acl prometheus src 172.20.0.0/16 http_access allow manager prometheus ``` Contribution: ------------- Pull request and issues are very welcome. Copyright: ---------- [MIT License](https://opensource.org/licenses/MIT) prometheus-squid-exporter-1.8.2+ds/VERSION 0000664 0000000 0000000 00000000004 13605621457 0020363 0 ustar 00root root 0000000 0000000 1.8 prometheus-squid-exporter-1.8.2+ds/_config.yml 0000664 0000000 0000000 00000000033 13605621457 0021444 0 ustar 00root root 0000000 0000000 theme: jekyll-theme-minimal prometheus-squid-exporter-1.8.2+ds/collector/ 0000775 0000000 0000000 00000000000 13605621457 0021307 5 ustar 00root root 0000000 0000000 prometheus-squid-exporter-1.8.2+ds/collector/client.go 0000664 0000000 0000000 00000005765 13605621457 0023131 0 ustar 00root root 0000000 0000000 package collector import ( "bufio" "encoding/base64" "errors" "fmt" "github.com/boynux/squid-exporter/types" "io" "log" "net" "net/http" "strconv" "strings" ) /*CacheObjectClient holds information about squid manager */ type CacheObjectClient struct { hostname string port int basicAuthString string headers map[string]string } /*SquidClient provides functionality to fetch squid metrics */ type SquidClient interface { GetCounters() (types.Counters, error) } const ( requestProtocol = "GET cache_object://localhost/%s HTTP/1.0" ) func buildBasicAuthString(login string, password string) string { if len(login) == 0 { return "" } else { return base64.StdEncoding.EncodeToString([]byte(login + ":" + password)) } } /*NewCacheObjectClient initializes a new cache client */ func NewCacheObjectClient(hostname string, port int, login string, password string) *CacheObjectClient { return &CacheObjectClient{ hostname, port, buildBasicAuthString(login, password), map[string]string{}, } } /*GetCounters fetches counters from squid cache manager */ func (c *CacheObjectClient) GetCounters() (types.Counters, error) { conn, err := connect(c.hostname, c.port) if err != nil { return types.Counters{}, err } r, err := get(conn, "counters", c.basicAuthString) if err != nil { return nil, err } if r.StatusCode != 200 { return nil, fmt.Errorf("Non success code %d while fetching metrics", r.StatusCode) } var counters types.Counters // TODO: Move to another func reader := bufio.NewReader(r.Body) for { line, err := reader.ReadString('\n') if err == io.EOF { break } if err != nil { return nil, err } c, err := decodeCounterStrings(line) if err != nil { log.Println(err) } else { counters = append(counters, c) } } return counters, err } func connect(hostname string, port int) (net.Conn, error) { return net.Dial("tcp", fmt.Sprintf("%s:%d", hostname, port)) } func get(conn net.Conn, path string, basicAuthString string) (*http.Response, error) { rBody := []string{ fmt.Sprintf(requestProtocol, path), "Host: localhost", "User-Agent: squidclient/3.5.12", } if len(basicAuthString) > 0 { rBody = append(rBody, "Proxy-Authorization: Basic "+basicAuthString) } rBody = append(rBody, "Accept: */*", "\r\n") request := strings.Join(rBody, "\r\n") fmt.Fprintf(conn, request) return http.ReadResponse(bufio.NewReader(conn), nil) } func decodeCounterStrings(line string) (types.Counter, error) { if equal := strings.Index(line, "="); equal >= 0 { if key := strings.TrimSpace(line[:equal]); len(key) > 0 { value := "" if len(line) > equal { value = strings.TrimSpace(line[equal+1:]) } // Remove additional formating string from `sample_time` if slices := strings.Split(value, " "); len(slices) > 0 { value = slices[0] } if i, err := strconv.ParseFloat(value, 64); err == nil { return types.Counter{key, i}, nil } } } return types.Counter{}, errors.New("could not parse line: " + line) } prometheus-squid-exporter-1.8.2+ds/collector/counters.go 0000664 0000000 0000000 00000005524 13605621457 0023506 0 ustar 00root root 0000000 0000000 package collector import ( "fmt" "strings" "github.com/prometheus/client_golang/prometheus" ) type squidCounter struct { Section string Counter string Suffix string Description string } var squidCounters = []squidCounter{ {"client_http", "requests", "total", "The total number of client requests"}, {"client_http", "hits", "total", "The total number of client cache hits"}, {"client_http", "errors", "total", "The total number of client http errors"}, {"client_http", "kbytes_in", "kbytes_total", "The total number of client kbytes received"}, {"client_http", "kbytes_out", "kbytes_total", "The total number of client kbytes transferred"}, {"client_http", "hit_kbytes_out", "bytes_total", "The total number of client kbytes cache hit"}, {"server.http", "requests", "total", "The total number of server http requests"}, {"server.http", "errors", "total", "The total number of server http errors"}, {"server.http", "kbytes_in", "kbytes_total", "The total number of server http kbytes received"}, {"server.http", "kbytes_out", "kbytes_total", "The total number of server http kbytes transferred"}, {"server.all", "requests", "total", "The total number of server all requests"}, {"server.all", "errors", "total", "The total number of server all errors"}, {"server.all", "kbytes_in", "kbytes_total", "The total number of server kbytes received"}, {"server.all", "kbytes_out", "kbytes_total", "The total number of server kbytes transferred"}, {"server.ftp", "requests", "total", "The total number of server ftp requests"}, {"server.ftp", "errors", "total", "The total number of server ftp errors"}, {"server.ftp", "kbytes_in", "kbytes_total", "The total number of server ftp kbytes received"}, {"server.ftp", "kbytes_out", "kbytes_total", "The total number of server ftp kbytes transferred"}, {"server.other", "requests", "total", "The total number of server other requests"}, {"server.other", "errors", "total", "The total number of server other errors"}, {"server.other", "kbytes_in", "kbytes_total", "The total number of server other kbytes received"}, {"server.other", "kbytes_out", "kbytes_total", "The total number of server other kbytes transferred"}, {"swap", "ins", "total", "The number of objects read from disk"}, {"swap", "outs", "total", "The number of objects saved to disk"}, {"swap", "files_cleaned", "total", "The number of orphaned cache files removed by the periodic cleanup procedure"}, } func generateSquidCounters(labels []string) descMap { counters := descMap{} for i := range squidCounters { counter := squidCounters[i] counters[fmt.Sprintf("%s.%s", counter.Section, counter.Counter)] = prometheus.NewDesc( prometheus.BuildFQName(namespace, strings.Replace(counter.Section, ".", "_", -1), fmt.Sprintf("%s_%s", counter.Counter, counter.Suffix)), counter.Description, labels, nil, ) } return counters } prometheus-squid-exporter-1.8.2+ds/collector/metrics.go 0000664 0000000 0000000 00000003322 13605621457 0023304 0 ustar 00root root 0000000 0000000 package collector import ( "log" "time" "github.com/boynux/squid-exporter/config" "github.com/prometheus/client_golang/prometheus" ) type descMap map[string]*prometheus.Desc const ( namespace = "squid" timeout = 10 * time.Second ) var ( counters descMap ) /*Exporter entry point to squid exporter */ type Exporter struct { client SquidClient hostname string port int labels config.Labels up *prometheus.GaugeVec } /*New initializes a new exporter */ func New(hostname string, port int, login string, password string, labels config.Labels) *Exporter { counters = generateSquidCounters(labels.Keys) c := NewCacheObjectClient(hostname, port, login, password) return &Exporter{ c, hostname, port, labels, prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, Name: "up", Help: "Was the last query of squid successful?", }, []string{"host"}), } } // Describe describes all the metrics ever exported by the ECS exporter. It // implements prometheus.Collector. func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { e.up.Describe(ch) for _, v := range counters { ch <- v } } /*Collect fetches metrics from squid manager and pushes them to promethus */ func (e *Exporter) Collect(c chan<- prometheus.Metric) { insts, err := e.client.GetCounters() if err == nil { e.up.With(prometheus.Labels{"host": e.hostname}).Set(1) for i := range insts { if d, ok := counters[insts[i].Key]; ok { c <- prometheus.MustNewConstMetric(d, prometheus.CounterValue, insts[i].Value, e.labels.Values...) } } } else { e.up.With(prometheus.Labels{"host": e.hostname}).Set(0) log.Println("Could not fetch metrics from squid instance: ", err) } e.up.Collect(c) } prometheus-squid-exporter-1.8.2+ds/config/ 0000775 0000000 0000000 00000000000 13605621457 0020566 5 ustar 00root root 0000000 0000000 prometheus-squid-exporter-1.8.2+ds/config/config.go 0000664 0000000 0000000 00000005675 13605621457 0022377 0 ustar 00root root 0000000 0000000 package config import ( "fmt" "flag" "log" "os" "strconv" "strings" ) const ( defaultListenAddress = "127.0.0.1:9301" defaultListenPort = 9301 defaultMetricsPath = "/metrics" defaultSquidHostname = "localhost" defaultSquidPort = 3128 ) const ( squidExporterListenKey = "SQUID_EXPORTER_LISTEN" squidExporterMetricsPathKey = "SQUID_EXPORTER_METRICS_PATH" squidHostnameKey = "SQUID_HOSTNAME" squidPortKey = "SQUID_PORT" squidLoginKey = "SQUID_LOGIN" squidPasswordKey = "SQUID_PASSWORD" ) var ( VersionFlag *bool ) type Labels struct { Keys []string Values []string } /*Config configurations for exporter */ type Config struct { ListenAddress string ListenPort int MetricPath string Labels Labels SquidHostname string SquidPort int Login string Password string } /*NewConfig creates a new config object from command line args */ func NewConfig() *Config { c := &Config{} flag.StringVar(&c.ListenAddress, "listen", loadEnvStringVar(squidExporterListenKey, defaultListenAddress), "Address and Port to bind exporter, in host:port format") flag.StringVar(&c.MetricPath, "metrics-path", loadEnvStringVar(squidExporterMetricsPathKey, defaultMetricsPath), "Metrics path to expose prometheus metrics") flag.Var(&c.Labels, "label", "Custom metrics to attach to metrics, use -label multiple times for each additional label") flag.StringVar(&c.SquidHostname, "squid-hostname", loadEnvStringVar(squidHostnameKey, defaultSquidHostname), "Squid hostname") flag.IntVar(&c.SquidPort, "squid-port", loadEnvIntVar(squidPortKey, defaultSquidPort), "Squid port to read metrics") flag.StringVar(&c.Login, "squid-login", loadEnvStringVar(squidLoginKey, ""), "Login to squid service") flag.StringVar(&c.Password, "squid-password", loadEnvStringVar(squidPasswordKey, ""), "Password to squid service") VersionFlag = flag.Bool("version", false, "Print the version and exit") flag.Parse() return c } func loadEnvStringVar(key, def string) string { val := os.Getenv(key) if val == "" { return def } return val } func loadEnvIntVar(key string, def int) int { valStr := os.Getenv(key) if valStr != "" { val, err := strconv.ParseInt(valStr, 0, 32) if err == nil { return int(val) } log.Printf("Error parsing %s='%s'. Integer value expected", key, valStr) } return def } func (l *Labels) String() string { var lbls []string for i := range l.Keys { lbls = append(lbls, l.Keys[i] + "=" + l.Values[i]) } return strings.Join(lbls, ", ") } func (l *Labels) Set(value string) error { args := strings.Split(value, "=") if len(args) != 2 || len(args[1]) < 1 { return fmt.Errorf("Label must be in 'key=value' format") } for _, key := range l.Keys { if key == args[0] { return fmt.Errorf("Labels must be distinct, found duplicated key %s", args[0]) } } l.Keys = append(l.Keys, args[0]) l.Values = append(l.Values, args[1]) return nil } prometheus-squid-exporter-1.8.2+ds/dashboards/ 0000775 0000000 0000000 00000000000 13605621457 0021433 5 ustar 00root root 0000000 0000000 prometheus-squid-exporter-1.8.2+ds/dashboards/squid-sample-dashboard.json 0000664 0000000 0000000 00000033003 13605621457 0026656 0 ustar 00root root 0000000 0000000 { "__inputs": [], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "5.2.4" }, { "type": "panel", "id": "graph", "name": "Graph", "version": "5.0.0" }, { "type": "panel", "id": "singlestat", "name": "Singlestat", "version": "5.0.0" }, { "type": "panel", "id": "text", "name": "Text", "version": "5.0.0" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "id": null, "iteration": 1536353270000, "links": [], "panels": [ { "content": "