├── boards ├── .gitignore ├── default.yml └── shortcuts.yml ├── .gitignore ├── prometheus-explorer-example.png ├── go.mod ├── Dockerfile ├── go.sum ├── static ├── deps │ ├── Chart.min.css │ ├── seedrandom.min.js │ ├── chartjs-plugin-annotation.min.js │ └── js-yaml.min.js ├── explore.js └── board.js ├── LICENSE ├── README.md ├── board.tmpl └── explorer.go /boards/.gitignore: -------------------------------------------------------------------------------- 1 | *.yml 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /prometheus-explorer 2 | /*.pem 3 | -------------------------------------------------------------------------------- /prometheus-explorer-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spreadshirt/prometheus-explorer/HEAD/prometheus-explorer-example.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/heyLu/prometheus-explorer 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gorilla/handlers v1.5.1 7 | github.com/gorilla/mux v1.8.0 8 | ) 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.8-alpine 2 | 3 | WORKDIR /go/src/app 4 | 5 | CMD /go/src/app/prometheus-explorer -addr 0.0.0.0:12345 6 | 7 | COPY . . 8 | 9 | RUN go build . 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= 2 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 3 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= 4 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= 5 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 6 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | -------------------------------------------------------------------------------- /static/deps/Chart.min.css: -------------------------------------------------------------------------------- 1 | @keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0} -------------------------------------------------------------------------------- /boards/default.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | source: http://localhost:9090 3 | from: now-1h 4 | to: now 5 | max_series: 10 6 | variables: 7 | cluster: "eu" 8 | instance: "localhost" 9 | 10 | up: 11 | query: up 12 | label: job 13 | 14 | network-receive: 15 | query: rate(node_network_receive_bytes_total{device="wlp3s0"}[5m]) 16 | unit: bytes 17 | label: device 18 | 19 | network: 20 | unit: bytes 21 | label: device 22 | queries: 23 | - query: rate(node_network_receive_bytes_total{device="wlp3s0"}[5m]) 24 | - query: -rate(node_network_transmit_bytes_total{device="wlp3s0"}[5m]) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luna Stadler (sprd.net AG) 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 | 23 | -------------------------------------------------------------------------------- /boards/shortcuts.yml: -------------------------------------------------------------------------------- 1 | shortcuts: 2 | - regexp: "cpu of (.*)" 3 | query: rate(container_cpu_usage_seconds_total{cluster=~"${vars.cluster}.*", pod=~"${match[1]}.*", image!="", container!="POD"}[5m]) 4 | label: pod 5 | unit: seconds 6 | - regexp: "(avg|min|max) cpu of (.*)" 7 | query: ${match[1]}(rate(container_cpu_usage_seconds_total{cluster=~"${vars.cluster}.*", pod=~"${match[2]}.*", image!="", container!="POD"}[5m])) 8 | label: ${match[1]} 9 | unit: seconds 10 | - regexp: "cpu throttling of (.*)" 11 | query: rate(container_cpu_cfs_throttled_seconds_total{cluster=~"${vars.cluster}.*", pod=~"${match[1]}.*", image!="", container!="POD"}[5m]) 12 | unit: seconds 13 | label: pod 14 | - regexp: "memory of (.*)" 15 | query: container_memory_usage_bytes{cluster=~"${vars.cluster}.*", pod=~"${match[1]}.*", image!="", container!="POD"} 16 | unit: bytes 17 | label: pod 18 | - regexp: "network in of (.*)" 19 | query: rate(container_network_receive_bytes_total{cluster=~"${vars.cluster}.*", pod=~"${match[1]}.*"}[5m]) 20 | unit: bytes 21 | label: pod 22 | - regexp: "network out of (.*)" 23 | query: -rate(container_network_transmit_bytes_total{cluster=~"${vars.cluster}.*", pod=~"${match[1]}.*"}[5m]) 24 | unit: bytes 25 | label: pod 26 | -------------------------------------------------------------------------------- /static/deps/seedrandom.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b,c,d,e,f,g,h,i){function j(a){var b,c=a.length,e=this,f=0,g=e.i=e.j=0,h=e.S=[];for(c||(a=[c++]);d>f;)h[f]=f++;for(f=0;d>f;f++)h[f]=h[g=s&g+a[f%c]+(b=h[f])],h[g]=b;(e.g=function(a){for(var b,c=0,f=e.i,g=e.j,h=e.S;a--;)b=h[f=s&f+1],c=c*d+h[s&(h[f]=h[g=s&g+b])+(h[g]=b)];return e.i=f,e.j=g,c})(d)}function k(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(k(a[c],b-1))}catch(f){}return d.length?d:"string"==e?a:a+"\0"}function l(a,b){for(var c,d=a+"",e=0;ea;)a=(a+c)*d,b*=d,c=s.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b},o,"global"in f?f.global:this==c)};if(l(c[i](),b),g&&g.exports){g.exports=t;try{o=require("crypto")}catch(u){}}else h&&h.amd&&h(function(){return t})}(this,[],Math,256,6,52,"object"==typeof module&&module,"function"==typeof define&&define,"random"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prometheus-explorer 2 | 3 | Interactively explore metrics, create shortcuts for common metrics and 4 | reference existing visualizations. 5 | 6 | This is not intended to replace Grafana or Prometheus' expression 7 | browser, but to exist between them, for on-the-fly exploration and 8 | referencing of metrics that belong to many different services or boards. 9 | 10 | **Note:** This software is in an *alpha* state, so there will probably 11 | be missing features, bugs and other oddities. 12 | 13 | ![example screenshot](./prometheus-explorer-example.png) 14 | 15 | ## Features 16 | 17 | - ✨ quickly explore metrics interactively 18 | - ✨ search for metrics using regular expressions 19 | - ✨ define custom shortcuts for commonly used queries 20 | 21 | ```yaml 22 | variables: 23 | cluster: eu 24 | 25 | shortcuts: 26 | # cpu usage of pods of $service running in $cluster 27 | - regexp: "cpu of (.*)" 28 | query: rate(container_cpu_usage_seconds_total{cluster=~"${vars.cluster}.*", pod=~"${match[1]}.*", image!="", container!="POD"}[5m]) 29 | ``` 30 | - ✨ define custom metrics on-the-fly using [YAML](https://yaml.org/) 31 | 32 | ```yaml 33 | network: 34 | query: rate(node_network_receive_bytes_total{device="wlp3s0"}[5m]) 35 | unit: bytes 36 | label: device 37 | ``` 38 | - not implemented yet: 39 | - reference metrics from other boards, e.g. "http requests of this service" 40 | 41 | ## Installation 42 | 43 | ### Docker 44 | 45 | 1. build the docker image using `docker build -t prometheus-explorer:local .` 46 | 2. run `docker run -it --rm -p 12345:12345 prometheus-explorer:local` 47 | 3. visit 48 | - note that the default board expects a local Prometheus server at 49 | `localhost:9090` and [`node_exporter`](https://github.com/prometheus/node_exporter) 50 | for some metrics, so either set those up or point `default.source` 51 | in the config to your existing Prometheus server. 52 | 53 | ### Local build 54 | 55 | 1. `git clone https://github.com/spreadshirt/prometheus-explorer` 56 | 2. run `go build .` in the cloned directory 57 | 3. run `./prometheus-explorer` 58 | - if you want to move it around, you'll need `board.tmpl`, `static/` 59 | and at least `boards/default.yml` to be next to the binary 60 | 61 | ## FAQ 62 | 63 | - I am happy with Grafana, why do I need this? 64 | 65 | You don't. If you're happy with Grafana, keep using that. If you 66 | miss some of the interactive features mentioned above, maybe you'll 67 | find this project useful. 68 | - I am happy with Prometheus' [expression browser](https://prometheus.io/docs/visualization/browser/), 69 | why do I need this? 70 | 71 | You probably don't! Use what fits your workflow. 72 | 73 | ## License 74 | 75 | This project is licensed under the MIT License, with the exception of 76 | the vendored dependencies defined in go.mod and static/deps, which are 77 | subject to their own respective licenses. 78 | -------------------------------------------------------------------------------- /static/explore.js: -------------------------------------------------------------------------------- 1 | let searchFormEl = document.getElementById("search"); 2 | let searchEl = searchFormEl.querySelector("input"); 3 | let metricNamesEl = searchFormEl.querySelector("#metric-names"); 4 | let keepResultsOpen = false; 5 | let resultsEl = searchFormEl.querySelector("pre"); 6 | let durEl = searchFormEl.querySelector(".duration"); 7 | let errEl = searchFormEl.querySelector(".error"); 8 | 9 | searchEl.onblur = (ev) => { 10 | if (!keepResultsOpen) { 11 | resultsEl.style.display = "none"; 12 | } 13 | } 14 | 15 | resultsEl.onmousenter = (ev) => { keepResultsOpen = true; }; 16 | resultsEl.onmousedown = (ev) => { keepResultsOpen = true; }; 17 | resultsEl.onmouseup = (ev) => { keepResultsOpen = false; }; 18 | resultsEl.onmouseleave = (ev) => { 19 | if (!keepResultsOpen) { 20 | resultsEl.style.display = "none"; 21 | } 22 | keepResultsOpen = false; 23 | } 24 | 25 | searchEl.onkeydown = (ev) => { 26 | if (ev.key == "Enter") { 27 | ev.preventDefault(); 28 | 29 | if (searchEl.value == "") { 30 | resultsEl.innerHTML = ""; 31 | resultsEl.style.display = "none"; 32 | return; 33 | } 34 | 35 | if (searchEl.value.match(/=|"|\{/)) { 36 | listSeries(); 37 | } else { 38 | fetchMetricNames(); 39 | } 40 | } 41 | } 42 | 43 | function listSeries() { 44 | let u = new URL(`${config.defaults.source}/api/v1/series`); 45 | let searchStart = new Date(); 46 | searchStart.setHours(searchStart.getHours() - 6); 47 | u.searchParams.set("match[]", searchEl.value); 48 | u.searchParams.set("start", searchStart.toISOString()); 49 | u.searchParams.set("end", new Date().toISOString()); 50 | let request = new Request(u.toString()); 51 | 52 | let start = new Date(); 53 | durEl.textContent = "searching..."; 54 | errEl.textContent = ""; 55 | resultsEl.innerHTML = ""; 56 | fetch(request) 57 | .then(resp => resp.json()) 58 | .then(resp => { 59 | if (resp.status != "success") { 60 | throw JSON.stringify(resp); 61 | } 62 | return resp; 63 | }) 64 | .then(resp => { 65 | durEl.textContent = (new Date() - start) + "ms"; 66 | 67 | for (let metric of resp.data) { 68 | let resultEl = document.createElement("span"); 69 | let nameEl = document.createElement("span"); 70 | nameEl.classList.add("name"); 71 | nameEl.textContent = metric.__name__; 72 | delete metric.__name__; 73 | resultEl.appendChild(nameEl); 74 | resultEl.appendChild(document.createTextNode(" ")); 75 | let tagsEl = document.createElement("span"); 76 | tagsEl.textContent = JSON.stringify(metric); 77 | resultEl.appendChild(tagsEl); 78 | resultEl.appendChild(document.createTextNode("\n")); 79 | resultsEl.appendChild(resultEl); 80 | 81 | resultsEl.style.display = "inherit"; 82 | } 83 | 84 | if (resp.data.length == 0) { 85 | errEl.textContent = "no results"; 86 | resultsEl.style.display = "none"; 87 | } 88 | }) 89 | .catch(err => { 90 | durEl.textContent = (new Date() - start) + "ms"; 91 | errEl.textContent = err; 92 | }); 93 | 94 | } 95 | 96 | function fetchMetricNames() { 97 | let u = new URL(`${location.protocol}//${location.host}/names`); 98 | u.searchParams.set("source", config.defaults.source); 99 | u.searchParams.set("match", searchEl.value); 100 | let request = new Request(u.toString()); 101 | 102 | let start = new Date(); 103 | durEl.textContent = "searching..."; 104 | errEl.textContent = ""; 105 | resultsEl.innerHTML = ""; 106 | fetch(request) 107 | .then(resp => resp.json()) 108 | .then(resp => { 109 | durEl.textContent = (new Date() - start) + "ms"; 110 | 111 | for (let name of resp.data) { 112 | let resultEl = document.createElement("span"); 113 | resultEl.textContent = name + "\n"; 114 | resultsEl.appendChild(resultEl); 115 | } 116 | resultsEl.style.display = "inherit"; 117 | 118 | if (resp.data.length == 0) { 119 | errEl.textContent = "no results"; 120 | resultsEl.style.display = "none"; 121 | } 122 | }) 123 | .catch(err => { 124 | durEl.textContent = (new Date() - start) + "ms"; 125 | errEl.textContent = err; 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /board.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ .Title }} - prometheus-explorer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 122 | 123 | 124 | 125 | 147 | 148 |
149 | Temporary overrides: 150 |
151 | 152 |

{{ .Title }}

153 | 154 |
155 |
156 | config 157 | 158 |
159 | 160 |
161 |
162 |
163 | 164 |
165 |

166 | 	
167 | 168 |
169 | 170 | {{ if (ne .Title "shortcuts") }} 171 |
172 | shortcuts 173 | 174 |

Go to /shortcuts to change the global shortcuts.

175 |
176 | {{ end }} 177 | 178 | 179 | -------------------------------------------------------------------------------- /explorer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "html/template" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "sync" 17 | 18 | "github.com/gorilla/handlers" 19 | "github.com/gorilla/mux" 20 | ) 21 | 22 | var config struct { 23 | Addr string 24 | BoardsDir string 25 | 26 | CertFile string 27 | KeyFile string 28 | } 29 | 30 | var boardTmpl *template.Template 31 | 32 | func main() { 33 | flag.StringVar(&config.Addr, "addr", "localhost:12345", "The address to listen on.") 34 | flag.StringVar(&config.BoardsDir, "boards", "./boards", "The directory the boards are stored in.") 35 | flag.StringVar(&config.CertFile, "cert-file", "", "The HTTPS certificate to use") 36 | flag.StringVar(&config.KeyFile, "key-file", "", "The HTTPS certificate key to use") 37 | flag.Parse() 38 | 39 | var err error 40 | boardTmpl, err = template.ParseFiles("board.tmpl") 41 | if err != nil { 42 | log.Fatalf("invalid template: %s", err) 43 | } 44 | 45 | r := mux.NewRouter() 46 | r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) 47 | r.HandleFunc("/names", handleNameAutocomplete).Methods("GET") 48 | r.HandleFunc("/{name}", renderBoard).Methods("GET") 49 | r.HandleFunc("/{name}", saveBoard).Methods("POST") 50 | r.HandleFunc("/{name}/{image}.png", handleImage).Methods("GET") 51 | r.HandleFunc("/{name}/{image}.png", saveImage).Methods("POST") 52 | r.HandleFunc("/", renderBoard).Methods("GET") 53 | 54 | if config.CertFile == "" || config.KeyFile == "" { 55 | log.Printf("Listening on http://%s", config.Addr) 56 | log.Fatal(http.ListenAndServe(config.Addr, handlers.CompressHandler(r))) 57 | } else { 58 | log.Printf("Listening on https://%s", config.Addr) 59 | log.Fatal(http.ListenAndServeTLS(config.Addr, config.CertFile, config.KeyFile, handlers.CompressHandler(r))) 60 | 61 | } 62 | } 63 | 64 | func renderBoard(w http.ResponseWriter, req *http.Request) { 65 | if req.URL.Query().Get("board") != "" { 66 | http.Redirect(w, req, "/"+req.URL.Query().Get("board"), http.StatusSeeOther) 67 | return 68 | } 69 | 70 | name := mux.Vars(req)["name"] 71 | if name == "" { 72 | name = "default" 73 | } 74 | 75 | isNew := false 76 | 77 | boardFiles, err := os.ReadDir(config.BoardsDir) 78 | if err != nil { 79 | log.Printf("listing boards: %s", err) 80 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 81 | return 82 | } 83 | 84 | boards := make([]string, 0, len(boardFiles)) 85 | for _, boardFile := range boardFiles { 86 | if boardFile.IsDir() { 87 | continue 88 | } 89 | 90 | board := boardFile.Name() 91 | if board == "default.yml" || board == "shortcuts.yml" { 92 | continue 93 | } 94 | 95 | if strings.HasSuffix(board, ".yml") { 96 | boards = append(boards, board[:len(board)-4]) 97 | } 98 | } 99 | sort.Strings(boards) 100 | boards = append(boards, "default", "shortcuts") 101 | 102 | // FIXME: restrict paths to only boards/ 103 | data, err := ioutil.ReadFile(path.Join(config.BoardsDir, name+".yml")) 104 | if err != nil { 105 | log.Printf("opening board %q: %s", name, err) 106 | 107 | isNew = true 108 | data, err = ioutil.ReadFile(path.Join(config.BoardsDir, "default.yml")) 109 | if err != nil { 110 | log.Printf("opening board %q: %s", name, err) 111 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 112 | return 113 | } 114 | } 115 | 116 | shortcutsData, err := ioutil.ReadFile(path.Join(config.BoardsDir, "shortcuts.yml")) 117 | if err != nil { 118 | log.Printf("opening shortcuts %q: %s", name, err) 119 | shortcutsData = []byte{} 120 | } 121 | 122 | boardTmpl.Execute(w, map[string]interface{}{ 123 | "Boards": boards, 124 | "Title": name, 125 | "IsNew": isNew, 126 | "Config": string(data), 127 | "Shortcuts": string(shortcutsData), 128 | }) 129 | } 130 | 131 | func saveBoard(w http.ResponseWriter, req *http.Request) { 132 | boardName := mux.Vars(req)["name"] 133 | 134 | boardConfig := req.FormValue("config") 135 | 136 | if strings.TrimSpace(boardConfig) == "" { 137 | http.Error(w, "empty board config", http.StatusBadRequest) 138 | return 139 | } 140 | 141 | err := ioutil.WriteFile(path.Join(config.BoardsDir, boardName+".yml"), []byte(strings.ReplaceAll(boardConfig, "\r\n", "\n")), 0644) 142 | if err != nil { 143 | log.Printf("could not save board: %s", err) 144 | http.Error(w, "error saving board", http.StatusInternalServerError) 145 | return 146 | } 147 | 148 | redirect := req.URL.Path 149 | if boardName == "default" { 150 | redirect = "/" 151 | } 152 | http.Redirect(w, req, redirect, http.StatusSeeOther) 153 | } 154 | 155 | func handleImage(w http.ResponseWriter, req *http.Request) { 156 | boardName := mux.Vars(req)["name"] 157 | imageName := mux.Vars(req)["image"] 158 | http.ServeFile(w, req, path.Join("/tmp", boardName+"__"+imageName+".png")) 159 | } 160 | 161 | func saveImage(w http.ResponseWriter, req *http.Request) { 162 | boardName := mux.Vars(req)["name"] 163 | imageName := mux.Vars(req)["image"] 164 | f, err := os.OpenFile(path.Join("/tmp", boardName+"__"+imageName+".png"), os.O_RDWR|os.O_CREATE, 0644) 165 | if err != nil { 166 | log.Printf("save image: %s", err) 167 | http.Error(w, err.Error(), http.StatusInternalServerError) 168 | return 169 | } 170 | defer f.Close() 171 | 172 | n, err := io.Copy(f, req.Body) 173 | if err != nil { 174 | log.Printf("save image: %s", err) 175 | http.Error(w, err.Error(), http.StatusInternalServerError) 176 | return 177 | } 178 | if n == 0 { 179 | http.Error(w, "no image data", http.StatusBadRequest) 180 | return 181 | } 182 | } 183 | 184 | var namesCacheMu sync.Mutex 185 | var namesCache = map[string][]string{} 186 | 187 | func handleNameAutocomplete(w http.ResponseWriter, req *http.Request) { 188 | source := req.URL.Query().Get("source") 189 | match := req.URL.Query().Get("match") 190 | 191 | matchRE, err := regexp.Compile(match) 192 | if err != nil { 193 | log.Printf("compiling match: %s", err) 194 | return 195 | } 196 | 197 | namesCacheMu.Lock() 198 | cachedNames, cached := namesCache[source] 199 | namesCacheMu.Unlock() 200 | 201 | enc := json.NewEncoder(w) 202 | 203 | if !cached { 204 | resp, err := http.Get(source + "/api/v1/label/__name__/values") 205 | if err != nil { 206 | log.Printf("getting names: %s", err) 207 | return 208 | } 209 | defer resp.Body.Close() 210 | 211 | dec := json.NewDecoder(resp.Body) 212 | var namesResp namesResponse 213 | err = dec.Decode(&namesResp) 214 | if err != nil { 215 | log.Printf("decoding names: %s", err) 216 | return 217 | } 218 | 219 | namesCacheMu.Lock() 220 | namesCache[source] = namesResp.Data 221 | namesCacheMu.Unlock() 222 | 223 | cachedNames = namesResp.Data 224 | } 225 | 226 | names := make([]string, 0, len(cachedNames)) 227 | for _, name := range cachedNames { 228 | if matchRE.MatchString(name) { 229 | names = append(names, name) 230 | } 231 | } 232 | err = enc.Encode(namesResponse{ 233 | Status: "success", 234 | Data: names, 235 | }) 236 | if err != nil { 237 | log.Printf("encoding names: %s", err) 238 | } 239 | } 240 | 241 | type namesResponse struct { 242 | Status string `json:"status"` 243 | Data []string `json:"data"` 244 | } 245 | -------------------------------------------------------------------------------- /static/deps/chartjs-plugin-annotation.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * chartjs-plugin-annotation.js 3 | * http://chartjs.org/ 4 | * Version: 0.5.7 5 | * 6 | * Copyright 2016 Evert Timberg 7 | * Released under the MIT license 8 | * https://github.com/chartjs/Chart.Annotation.js/blob/master/LICENSE.md 9 | */ 10 | !function e(t,n,i){function o(r,l){if(!n[r]){if(!t[r]){var s="function"==typeof require&&require;if(!l&&s)return s(r,!0);if(a)return a(r,!0);var c=new Error("Cannot find module '"+r+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[r]={exports:{}};t[r][0].call(u.exports,function(e){var n=t[r][1][e];return o(n?n:e)},u,u.exports,e,t,n,i)}return n[r].exports}for(var a="function"==typeof require&&require,r=0;r0){var n=e.chart.canvas,i=r.dispatcher.bind(e);r.collapseHoverEvents(t).forEach(function(t){o.addEvent(n,t,i),e.annotation.onDestroy.push(function(){o.removeEvent(n,t,i)})})}},destroy:function(e){for(var t=e.annotation.onDestroy;t.length>0;)t.pop()()}}}},{"./events.js":4,"./helpers.js":5}],3:[function(e,t,n){t.exports=function(e){var t=e.helpers,n=e.Element.extend({initialize:function(){this.hidden=!1,this.hovering=!1,this._model=t.clone(this._model)||{},this.setDataLimits()},destroy:function(){},setDataLimits:function(){},configure:function(){},inRange:function(){},getCenterPoint:function(){},getWidth:function(){},getHeight:function(){},getArea:function(){},draw:function(){}});return n}},{}],4:[function(e,t,n){t.exports=function(t){function n(e){var t=!1,n=e.filter(function(e){switch(e){case"mouseenter":case"mouseover":case"mouseout":case"mouseleave":return t=!0,!1;default:return!0}});return t&&n.indexOf("mousemove")===-1&&n.push("mousemove"),n}function i(e){var t=this.annotation,i=a.elements(this),r=o.getRelativePosition(e,this.chart),l=a.getNearestItems(i,r),s=n(t.options.events),c=t.options.dblClickSpeed,u=[],f=a.getEventHandlerName(e.type),d=(l||{}).options;if("mousemove"===e.type&&(l&&!l.hovering?["mouseenter","mouseover"].forEach(function(t){var n=a.getEventHandlerName(t),i=a.createMouseEvent(t,e);l.hovering=!0,"function"==typeof d[n]&&u.push([d[n],i,l])}):l||i.forEach(function(t){if(t.hovering){t.hovering=!1;var n=t.options;["mouseout","mouseleave"].forEach(function(i){var o=a.getEventHandlerName(i),r=a.createMouseEvent(i,e);"function"==typeof n[o]&&u.push([n[o],r,t])})}})),l&&s.indexOf("dblclick")>-1&&"function"==typeof d.onDblclick){if("click"===e.type&&"function"==typeof d.onClick)return clearTimeout(l.clickTimeout),l.clickTimeout=setTimeout(function(){delete l.clickTimeout,d.onClick.call(l,e)},c),e.stopImmediatePropagation(),void e.preventDefault();"dblclick"===e.type&&l.clickTimeout&&(clearTimeout(l.clickTimeout),delete l.clickTimeout)}l&&"function"==typeof d[f]&&0===u.length&&u.push([d[f],e,l]),u.length>0&&(e.stopImmediatePropagation(),e.preventDefault(),u.forEach(function(e){e[0].call(e[2],e[1])}))}var o=t.helpers,a=e("./helpers.js")(t);return{dispatcher:i,collapseHoverEvents:n}}},{"./helpers.js":5}],5:[function(e,t,n){function i(){}function o(e){var t=e.annotation.elements;return Object.keys(t).map(function(e){return t[e]})}function a(){return Math.random().toString(36).substr(2,6)}function r(e){return null!==e&&"undefined"!=typeof e&&("number"==typeof e?isFinite(e):!!e)}function l(e,t,n){var i="$";e[i+t]||(e[t]?(e[i+t]=e[t].bind(e),e[t]=function(){var o=[e[i+t]].concat(Array.prototype.slice.call(arguments));return n.apply(e,o)}):e[t]=function(){var t=[void 0].concat(Array.prototype.slice.call(arguments));return n.apply(e,t)})}function s(e,t){e.forEach(function(e){(t?e[t]:e)()})}function c(e){return"on"+e[0].toUpperCase()+e.substring(1)}function u(e,t){try{return new MouseEvent(e,t)}catch(n){try{var i=document.createEvent("MouseEvent");return i.initMouseEvent(e,t.canBubble,t.cancelable,t.view,t.detail,t.screenX,t.screenY,t.clientX,t.clientY,t.ctrlKey,t.altKey,t.shiftKey,t.metaKey,t.button,t.relatedTarget),i}catch(o){var a=document.createEvent("Event");return a.initEvent(e,t.canBubble,t.cancelable),a}}}t.exports=function(e){function t(t){return t=h.configMerge(e.Annotation.defaults,t),h.isArray(t.annotations)&&t.annotations.forEach(function(t){t.label=h.configMerge(e.Annotation.labelDefaults,t.label)}),t}function n(e,t,n,i){var o=t.filter(function(t){return!!t._model.ranges[e]}).map(function(t){return t._model.ranges[e]}),a=o.map(function(e){return Number(e.min)}).reduce(function(e,t){return isFinite(t)&&!isNaN(t)&&te?t:e},i);return{min:a,max:r}}function f(e){var t=n(e.id,o(e.chart),e.min,e.max);"undefined"==typeof e.options.ticks.min&&"undefined"==typeof e.options.ticks.suggestedMin&&(e.min=t.min),"undefined"==typeof e.options.ticks.max&&"undefined"==typeof e.options.ticks.suggestedMax&&(e.max=t.max),e.handleTickRangeOptions&&e.handleTickRangeOptions()}function d(e,t){var n=Number.POSITIVE_INFINITY;return e.filter(function(e){return e.inRange(t.x,t.y)}).reduce(function(e,i){var o=i.getCenterPoint(),a=h.distanceBetweenPoints(t,o);return ai||n=n.left&&e<=n.right&&t>=n.top&&t<=n.bottom},getCenterPoint:function(){var e=this._model;return{x:(e.right+e.left)/2,y:(e.bottom+e.top)/2}},getWidth:function(){var e=this._model;return Math.abs(e.right-e.left)},getHeight:function(){var e=this._model;return Math.abs(e.bottom-e.top)},getArea:function(){return this.getWidth()*this.getHeight()},draw:function(){var e=this._view,t=this.chartInstance.chart.ctx;t.save(),t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,t.fillStyle=e.backgroundColor;var n=e.right-e.left,i=e.bottom-e.top;t.fillRect(e.left,e.top,n,i),t.strokeRect(e.left,e.top,n,i),t.restore()}});return i}},{"../helpers.js":5}],8:[function(e,t,n){t.exports=function(t){function n(e){var t=(e.x2-e.x1)/(e.y2-e.y1),n=e.x1||0;this.m=t,this.b=n,this.getX=function(i){return t*(i-e.y1)+n},this.getY=function(i){return(i-n)/t+e.y1},this.intersects=function(e,t,n){n=n||.001;var i=this.getY(e),o=this.getX(t);return(!isFinite(i)||Math.abs(t-i)=n.labelX&&e<=n.labelX+n.labelWidth&&t>=n.labelY&&t<=n.labelY+n.labelHeight},getCenterPoint:function(){return{x:(this._model.x2+this._model.x1)/2,y:(this._model.y2+this._model.y1)/2}},getWidth:function(){return Math.abs(this._model.right-this._model.left)},getHeight:function(){return this._model.borderWidth||1},getArea:function(){return Math.sqrt(Math.pow(this.getWidth(),2)+Math.pow(this.getHeight(),2))},draw:function(){var e=this._view,t=this.chartInstance.chart.ctx;e.clip&&(t.save(),t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.lineWidth=e.borderWidth,t.strokeStyle=e.borderColor,t.setLineDash&&t.setLineDash(e.borderDash),t.lineDashOffset=e.borderDashOffset,t.beginPath(),t.moveTo(e.x1,e.y1),t.lineTo(e.x2,e.y2),t.stroke(),e.labelEnabled&&e.labelContent&&(t.beginPath(),t.rect(e.clip.x1,e.clip.y1,e.clip.x2-e.clip.x1,e.clip.y2-e.clip.y1),t.clip(),t.fillStyle=e.labelBackgroundColor,o.drawRoundedRectangle(t,e.labelX,e.labelY,e.labelWidth,e.labelHeight,e.labelCornerRadius),t.fill(),t.font=o.fontString(e.labelFontSize,e.labelFontStyle,e.labelFontFamily),t.fillStyle=e.labelFontColor,t.textAlign="center",t.textBaseline="middle",t.fillText(e.labelContent,e.labelX+e.labelWidth/2,e.labelY+e.labelHeight/2)),t.restore())}});return s}},{"../helpers.js":5}]},{},[6]); -------------------------------------------------------------------------------- /static/board.js: -------------------------------------------------------------------------------- 1 | globalErrEl = document.getElementById("error"); 2 | window.onerror = (msg, url, line) => { 3 | globalErrEl.textContent = `${msg} (${url}:${line})`; 4 | }; 5 | 6 | let configEl = document.getElementById("config"); 7 | let config = {}; 8 | let configUpdated = new Date(); 9 | configEl.onchange = () => configUpdated = new Date(); 10 | let overridesUpdated = new Date(); 11 | let shortcutsEl = document.getElementById("shortcuts"); 12 | let chartsEl = document.getElementById("charts"); 13 | let charts = {}; 14 | 15 | let crosshairCharts = []; 16 | let crosshairChartsImages = []; 17 | let crosshairEvent = null; 18 | let crosshairEventChart = null; 19 | let crosshairPlugin = { 20 | beforeInit: function(chart) { 21 | crosshairCharts.push(chart); 22 | crosshairChartsImages.push(null); 23 | }, 24 | destroy: function(chart) { 25 | let i = 0; 26 | for (i = 0; i < crosshairCharts.length; i++) { 27 | if (chart.id == crosshairCharts[i].id) { 28 | break; 29 | } 30 | } 31 | crosshairCharts.splice(i, 1); 32 | crosshairChartsImages.splice(i, 1); 33 | }, 34 | beforeEvent: function(chart, ev) { 35 | if (ev.type != "mousemove" && ev.type != "mouseout") { 36 | return; 37 | } 38 | crosshairEvent = ev; 39 | crosshairEventChart = chart; 40 | }, 41 | afterDatasetsDraw: function(chart) { 42 | if (!crosshairEvent) { 43 | return; 44 | } 45 | 46 | if (crosshairEvent.type == "mouseout") { 47 | for (let i = 0; i < crosshairCharts.length; i++) { 48 | if (crosshairChartsImages[i] != null) { 49 | crosshairCharts[i].ctx.putImageData(crosshairChartsImages[i], 0, 0); 50 | } 51 | } 52 | return; 53 | } 54 | 55 | for (let i = 0; i < crosshairCharts.length; i++) { 56 | let chart = crosshairCharts[i]; 57 | 58 | let x = crosshairEvent.x; 59 | if (chart.id != crosshairEventChart.id) { 60 | if (crosshairChartsImages[i] == null) { 61 | crosshairChartsImages[i] = chart.ctx.getImageData(0, 0, chart.ctx.canvas.width, chart.ctx.canvas.height); 62 | } 63 | chart.ctx.putImageData(crosshairChartsImages[i], 0, 0); 64 | 65 | let origLeft = crosshairEventChart.chartArea.left; 66 | x = chart.chartArea.left + ((crosshairEvent.x - origLeft) / (crosshairEventChart.width - origLeft)) * (chart.width - chart.chartArea.left); 67 | } 68 | 69 | // ensure crosshair does not go out of chart range 70 | x = Math.min(x, chart.chartArea.right); 71 | x = Math.max(x, chart.chartArea.left); 72 | 73 | // avoid anti-aliasing blur (wtf) 74 | x = Chart.helpers._alignPixel(chart, x, chart.width); 75 | 76 | chart.ctx.save() 77 | chart.ctx.strokeStyle = "black"; 78 | let path = new Path2D(); 79 | path.moveTo(x, chart.chartArea.top); 80 | path.lineTo(x, chart.chartArea.bottom); 81 | chart.ctx.stroke(path); 82 | chart.ctx.restore(); 83 | } 84 | 85 | crosshairEvent = null; 86 | crosshairEventChart = null; 87 | }, 88 | beforeUpdate: function(chart) { 89 | for (let i = 0; i < crosshairCharts.length; i++) { 90 | if (chart.id == crosshairCharts[i].id) { 91 | crosshairChartsImages[i] = null; 92 | } 93 | } 94 | }, 95 | afterDraw: function(chart) { 96 | for (let i = 0; i < crosshairCharts.length; i++) { 97 | if (chart.id == crosshairCharts[i].id) { 98 | crosshairChartsImages[i] = null; 99 | } 100 | } 101 | }, 102 | }; 103 | 104 | update(); 105 | configEl.addEventListener("change", update); 106 | 107 | window.addEventListener("keydown", (ev) => { 108 | if (ev.ctrlKey && ev.key == "Enter") { 109 | if (ev.target == configEl) { 110 | configUpdated = new Date(); 111 | } 112 | 113 | globalErrEl.textContent = ""; 114 | update(); 115 | } 116 | }); 117 | 118 | function update() { 119 | config = jsyaml.load(configEl.value); 120 | 121 | // set overrides from query params 122 | // FIXME: potential confusion because query overrides config, even if config is more recent 123 | for (let [key, val] of new URL(location.href).searchParams.entries()) { 124 | if (key == "from" || key == "to") { 125 | key = "defaults." + key; 126 | } 127 | 128 | key.split(".").reduce((config, key, idx, arr) => { 129 | if (!config) { 130 | return null; 131 | } 132 | 133 | if (idx == arr.length-1) { 134 | config[key] = val; 135 | return null; 136 | } 137 | 138 | return config[key]; 139 | }, config); 140 | } 141 | 142 | // quick access overrides 143 | let overridesEl = document.getElementById("overrides"); 144 | function createOverride(name, initialValue, setValueFn) { 145 | let varEl = overridesEl.querySelector(`[data-name=${name}]`); 146 | if (varEl == null) { 147 | varEl = document.createElement("span"); 148 | varEl.dataset['name'] = name; 149 | 150 | let nameEl = document.createElement("label"); 151 | nameEl.textContent = name + ":"; 152 | varEl.appendChild(nameEl); 153 | varEl.appendChild(document.createTextNode(" ")); 154 | 155 | let valEl = document.createElement("input"); 156 | valEl.name = name; 157 | valEl.value = initialValue; 158 | valEl.size = valEl.value.length; 159 | valEl.onchange = function(ev) { 160 | valEl.size = valEl.value.length; 161 | 162 | overridesUpdated = new Date(); 163 | setValueFn(ev.target.value); 164 | 165 | update(); 166 | } 167 | varEl.appendChild(valEl); 168 | 169 | overridesEl.appendChild(varEl); 170 | } 171 | } 172 | 173 | createOverride("from", config.defaults.from, (val) => config.from = val); 174 | createOverride("to", config.defaults.to, (val) => config.defaults.to = val); 175 | for (let key in config.variables) { 176 | createOverride(key, config.variables[key], (val) => config.variables[key] = val); 177 | } 178 | if (overridesUpdated > configUpdated) { 179 | config.defaults.from = overridesEl.querySelector("[name=from]").value; 180 | config.defaults.to = overridesEl.querySelector("[name=to]").value; 181 | for (let key in config.variables) { 182 | // overrides value from config 183 | // TODO: notify about override in yaml somehow 184 | config.variables[key] = overridesEl.querySelector(`[name=${key}]`).value; 185 | } 186 | } else { 187 | overridesEl.querySelector("[name=from]").value = config.defaults.from; 188 | overridesEl.querySelector("[name=to]").value = config.defaults.to; 189 | for (let key in config.variables) { 190 | overridesEl.querySelector(`[name=${key}]`).value = config.variables[key]; 191 | } 192 | } 193 | 194 | config.shortcuts = config.shortcuts || []; 195 | let shortcuts = jsyaml.load(shortcutsEl.value); 196 | for (let shortcut of shortcuts.shortcuts) { 197 | config.shortcuts.push(shortcut); 198 | } 199 | 200 | config.annotations = config.annotations || []; 201 | for (let annotation of config.annotations) { 202 | let defaults = config.defaults; 203 | let url = eval("`"+annotation.url+"`"); 204 | fetch(new Request(url)) 205 | .then((resp) => resp.json()) 206 | .then((resp) => { 207 | annotation.data = resp.hits.hits.map((doc) => { 208 | let v = doc._source["@timestamp"] 209 | return {t: new Date(v.endsWith("Z") ? v : v+"Z")} 210 | }) 211 | 212 | for (let name in Chart.instances) { 213 | let myChart = Chart.instances[name]; 214 | myChart.options.annotation.annotations = []; 215 | for (let ann of annotation.data) { 216 | let color = annotation.color; 217 | if (!annotation.color) { 218 | Math.seedrandom(annotation.url); 219 | color = `hsl(${Math.floor(Math.random()*360)}, 90%, 50%)`; 220 | } 221 | myChart.options.annotation.annotations.push({ 222 | type: "line", 223 | scaleID: "x-axis-0", 224 | mode: "vertical", 225 | borderColor: color, 226 | borderWidth: 1, 227 | value: ann.t.getTime(), 228 | }); 229 | } 230 | myChart.update(); 231 | } 232 | }) 233 | .catch((err) => { throw err }); 234 | } 235 | 236 | for (let key in config) { 237 | if (key == "defaults" || key == "annotations" || key == "variables" || key == "shortcuts") { 238 | continue; 239 | } 240 | 241 | if (!(key in charts)) { 242 | createChart(key, config[key], config); 243 | } else { 244 | charts[key].render(key, config[key], config); 245 | } 246 | } 247 | 248 | for (let name in charts) { 249 | if (!(name in config)) { 250 | charts[name].chart.destroy(); 251 | chartsEl.removeChild(charts[name].element); 252 | delete charts[name]; 253 | } 254 | } 255 | } 256 | 257 | // TODO: support refreshing automatically 258 | /*window.setInterval(function() { 259 | for (chart of charts) { 260 | chart.render(); 261 | } 262 | }, 10 * 1000);*/ 263 | 264 | function createChart(name, config, global) { 265 | let vars = global.variables; 266 | 267 | let chartEl = document.createElement("article"); 268 | chartEl.classList.add("chart"); 269 | let titleEl = document.createElement("h1"); 270 | let titleNode = document.createTextNode(""); 271 | titleNode.textContent = eval("`"+(config.name || name)+"`"); 272 | let linkEl = document.createElement("a"); 273 | linkEl.textContent = "🔗"; 274 | let imageEl = document.createElement("a"); 275 | imageEl.textContent = "📄"; 276 | imageEl.title = "copy permalink to the rendered image"; 277 | titleEl.appendChild(titleNode); 278 | titleEl.appendChild(document.createTextNode(" ")); 279 | titleEl.appendChild(linkEl); 280 | titleEl.appendChild(document.createTextNode(" ")); 281 | titleEl.appendChild(imageEl); 282 | chartEl.appendChild(titleEl); 283 | let canvasEl = document.createElement("canvas"); 284 | canvasEl.width = 800; 285 | canvasEl.height = 400; 286 | let containerEl = document.createElement("div"); 287 | containerEl.classList.add("chart-container"); 288 | containerEl.appendChild(canvasEl); 289 | chartEl.appendChild(containerEl); 290 | let durEl = document.createElement("span"); 291 | durEl.classList.add("duration"); 292 | chartEl.appendChild(durEl); 293 | chartEl.appendChild(document.createTextNode(" ")); 294 | let errEl = document.createElement("span"); 295 | errEl.classList.add("error"); 296 | chartEl.appendChild(errEl); 297 | let generatedQueryEl = document.createElement("input"); 298 | generatedQueryEl.style.display = "none"; 299 | generatedQueryEl.disabled = "disabled"; 300 | chartEl.appendChild(generatedQueryEl); 301 | 302 | chartsEl.appendChild(chartEl); 303 | 304 | let numLabels = 0; 305 | var ctx = canvasEl.getContext("2d"); 306 | var myChart = new Chart(ctx, { 307 | type: 'line', 308 | data: { 309 | datasets: [], 310 | }, 311 | options: { 312 | scales: { 313 | xAxes: [{ 314 | type: "time", 315 | time: { 316 | // TODO: always display timestamps in utc 317 | displayFormats: { 318 | second: "HH:mm:ss", 319 | minute: "HH:mm", 320 | hour: "HH:00", 321 | day: "YYYY-MM-DD", 322 | week: "YYYY-MM-DD", 323 | month: "YYYY-MM", 324 | }, 325 | }, 326 | ticks: { 327 | min: toDate(config.from || global.defaults.from), 328 | max: toDate(config.to || global.defaults.to), 329 | }, 330 | }], 331 | yAxes: [{ 332 | stacked: config.stacked || global.defaults.stacked || false, 333 | ticks: { 334 | beginAtZero: true, 335 | callback: function(value, index, values) { 336 | return formatUnit(value, config.unit); 337 | }, 338 | } 339 | }] 340 | }, 341 | legend: { 342 | position: "bottom", 343 | labels: { 344 | boxWidth: 10, 345 | fontSize: 10, 346 | pointStyle: true, 347 | filter: function(legendItem, chart) { 348 | if (numLabels > 10) { 349 | return false; 350 | } 351 | numLabels++; 352 | return true; 353 | }, 354 | }, 355 | }, 356 | tooltips: { 357 | intersect: false, 358 | axis: "x", 359 | itemSort: function(a, b) { return b.yLabel - a.yLabel; }, 360 | position: "nearest", 361 | backgroundColor: 'rgba(0, 0, 0, 0.6)', 362 | callbacks: { 363 | label: function(tooltipItem, data) { 364 | let label = data.datasets[tooltipItem.datasetIndex].label || ''; 365 | if (label) { 366 | label += ': '; 367 | } 368 | 369 | let value = tooltipItem.yLabel; 370 | label += formatUnit(value, config.unit); 371 | return label; 372 | }, 373 | }, 374 | }, 375 | // plugin configuration 376 | annotation: { 377 | annotations: [/*{ 378 | type: "line", 379 | scaleID: "x-axis-0", 380 | mode: "vertical", 381 | borderColor: "black", 382 | borderWidth: 1, 383 | value: new Date(), 384 | label: { 385 | enabled: false, 386 | content: "Test annotation", 387 | position: "bottom", 388 | }, 389 | }*/], 390 | }, 391 | // performance optimizations (mostly no animations) 392 | elements: { 393 | line: { 394 | tension: 0, 395 | }, 396 | }, 397 | animation: { 398 | duration: 0, 399 | }, 400 | hover: { 401 | animationDuration: 0, 402 | }, 403 | responsiveAnimationDuration: 0, 404 | }, 405 | plugins: [crosshairPlugin], 406 | }); 407 | 408 | let chart = { 409 | config: config, 410 | chart: myChart, 411 | element: chartEl, 412 | render: (name, config, global) => render(name, config, global), 413 | }; 414 | config.name = name; 415 | charts[name] = chart; 416 | 417 | imageEl.addEventListener("click", function(ev) { 418 | ev.preventDefault(); 419 | 420 | let url = `${location.protocol}//${location.host}${location.pathname}/${name}-${new Date().getTime()}.png`; 421 | myChart.canvas.toBlob((blob) => { 422 | let req = new Request(url, {method: "POST", body: blob}) 423 | fetch(req) 424 | .then(() => { 425 | navigator.clipboard.writeText(url) 426 | .then(() => { 427 | imageEl.style.boxShadow = "-1px -1px 1px green, 1px 1px 1px green"; 428 | }) 429 | }) 430 | .catch((err) => { 431 | imageEl.style.boxShadow = "-1px -1px 1px red, 1px 1px 1px red"; 432 | console.error(err); 433 | }) 434 | .finally(() => { 435 | window.setTimeout(() => { 436 | imageEl.style.boxShadow = ""; 437 | }, 5000); 438 | }); 439 | }, "image/png"); 440 | }); 441 | 442 | render(name, config, global); 443 | 444 | async function render(name, config, global) { 445 | numLabels = 0; 446 | 447 | let vars = global.variables; 448 | 449 | // adjust chart options 450 | if (config.y_max) { 451 | myChart.options.scales.yAxes[0].ticks.max = config.y_max; 452 | } else { 453 | delete myChart.options.scales.yAxes[0].ticks.max; 454 | } 455 | 456 | let legend = config.legend || global.defaults.legend; 457 | if (legend != undefined) { 458 | myChart.options.legend.display = legend; 459 | } 460 | 461 | myChart.options.scales.yAxes[0].stacked = config.stacked || global.defaults.stacked || false; 462 | myChart.options.scales.yAxes[0].ticks.callback = function(value, index, values) { 463 | return formatUnit(value, config.unit); 464 | }; 465 | 466 | myChart.options.scales.xAxes[0].ticks.min = toDate(config.from || global.defaults.from); 467 | myChart.options.scales.xAxes[0].ticks.max = toDate(config.to || global.defaults.to); 468 | 469 | let queries = []; 470 | if (config.query) { 471 | queries.push({query: config.query}) 472 | } 473 | for (let query of (config.queries || [])) { 474 | queries.push(query); 475 | } 476 | 477 | titleNode.textContent = eval("`"+(config.name || name)+"`"); 478 | titleEl.title = queries.map(q => q.query).join("\n"); 479 | 480 | generatedQueryEl.value = ""; 481 | 482 | let datasets = []; 483 | for (let query of queries) { 484 | 485 | for (let shortcut of global.shortcuts) { 486 | let match = query.query.match(shortcut.regexp); 487 | if (match) { 488 | query.query = eval("`"+shortcut.query+"`"); 489 | generatedQueryEl.style.display = "block"; 490 | generatedQueryEl.value = query.query; 491 | generatedQueryEl.size = query.query.length; 492 | config.unit = shortcut.unit; 493 | config.label = shortcut.label; 494 | } 495 | } 496 | 497 | let label = query.label || config.label; 498 | if (label) { 499 | label = eval("`"+label+"`"); 500 | } 501 | query.query = eval("`"+query.query+"`"); 502 | durEl.textContent = "loading..."; 503 | let res = fetchDataset({ 504 | query: query.query, 505 | from: config.from, 506 | to: config.to, 507 | // FIXME: support display of all datasets (only displays one so far) 508 | label: label, 509 | unit: config.unit, 510 | max_series: config.max_series, 511 | y_max: config.y_max, 512 | }, global); 513 | datasets.push(res); 514 | } 515 | 516 | let chartURL = new URL(`${global.defaults.source}/graph`); 517 | queries.forEach((query, idx) => { 518 | chartURL.searchParams.set(`g${idx}.expr`, query.query); 519 | chartURL.searchParams.set(`g${idx}.tab`, "0"); // display graph 520 | }); 521 | linkEl.href = chartURL.toString(); 522 | 523 | let start = new Date(); 524 | durEl.textContent = "updating..."; 525 | errEl.textContent = ""; 526 | 527 | Promise.all(datasets).then((results) => { 528 | // reset dataset only after data has loaded (?) 529 | myChart.data.datasets = []; 530 | 531 | for (let result of results) { 532 | Array.prototype.push.apply(myChart.data.datasets, result.datasets); 533 | 534 | if (result.error) { 535 | errEl.textContent = result.error; 536 | } 537 | } 538 | 539 | myChart.update(); 540 | }).catch((err) => { 541 | errEl.textContent = err; 542 | }).finally(() => { 543 | durEl.textContent = new Date() - start + "ms"; 544 | }); 545 | } 546 | } 547 | 548 | async function fetchDataset(config, global) { 549 | let u = new URL(`${global.defaults.source}/api/v1/query_range`); 550 | u.searchParams.set("query", config.query); 551 | 552 | let from = toDate(config.from || global.defaults.from); 553 | let to = toDate(config.to || global.defaults.to); 554 | u.searchParams.set("start", toDateString(from)); 555 | u.searchParams.set("end", toDateString(to)); 556 | 557 | // step size in seconds chosen to yield 200 steps per chart 558 | let autoStep = Math.max(1, Math.round((to - from) / 1000 / 200)); 559 | let step = parseOffset(config.step || global.defaults.step || autoStep); 560 | u.searchParams.set("step", step); 561 | 562 | let request = new Request(u.toString()); 563 | let resp = await fetch(request) 564 | .then(resp => resp.json()) 565 | .then(resp => { 566 | if (resp.status != "success") { 567 | throw JSON.stringify(resp); 568 | } 569 | return resp; 570 | }); 571 | 572 | let datasets = []; 573 | let error = ""; 574 | 575 | let maxSeries = config.max_series || global.defaults.max_series; 576 | if (resp.data.result.length > maxSeries) { 577 | error = `too many metrics, displaying only first ${maxSeries} (of ${resp.data.result.length})`; 578 | } 579 | 580 | for (metric of resp.data.result.slice(0, maxSeries)) { 581 | let label = JSON.stringify(metric.metric); 582 | if (config.label != undefined) { 583 | label = metric.metric[config.label] || config.label; 584 | } 585 | Math.seedrandom(JSON.stringify(metric.metric)); 586 | if (Object.keys(metric.metric).length == 0) { 587 | Math.seedrandom(config.query); 588 | } 589 | let color = Math.floor(Math.random()*360); 590 | datasets.push({ 591 | label: label, 592 | data: metric.values 593 | .map(([t, v]) => ({t: new Date(t*1000), y: (Number.parseFloat(v))})) 594 | .map((kv, idx, arr) => { 595 | if (idx+1 >= arr.length) { 596 | return kv; 597 | } 598 | 599 | if (!isNaN(kv.y) && (arr[idx+1].t.getTime() - kv.t.getTime()) > step*1000) { 600 | arr.splice(idx+1, 0, {t: new Date(kv.t.getTime()+1), y: NaN}); 601 | } 602 | 603 | return kv; 604 | }), 605 | borderColor: `hsl(${color}, 90%, 50%)`, 606 | backgroundColor: `hsla(${color}, 90%, 50%, 0.3)`, 607 | pointRadius: 0, 608 | pointHoverRadius: 2, 609 | borderWidth: 1, 610 | }); 611 | } 612 | 613 | return { 614 | datasets: datasets, 615 | error: error, 616 | }; 617 | } 618 | 619 | // ported from https://github.com/spreadshirt/es-stream-logs/blob/7f320ff3d5d9abb454e69faba041e6a7f107710e/es-stream-logs.py#L201-L216 620 | function parseOffset(offset) { 621 | if (typeof offset == "number") { 622 | return offset; 623 | } 624 | let suffix = offset[offset.length-1]; 625 | let num = Number.parseInt(offset.substr(0, offset.length-1)); 626 | switch (suffix) { 627 | case "s": 628 | return num; 629 | case "m": 630 | return num * 60; 631 | case "h": 632 | return num * 60 * 60; 633 | case "d": 634 | return num * 24 * 60 * 60; 635 | default: 636 | throw `could not parse offset ${offset}`; 637 | } 638 | } 639 | 640 | function toDate(d) { 641 | if (d instanceof Date) { 642 | return d; 643 | } 644 | if (d == "now") { 645 | return new Date(); 646 | } 647 | if (d.startsWith("now-")) { 648 | return new Date((new Date().getTime()) - parseOffset(d.substr(4, d.length))*1000); 649 | } 650 | return new Date(d); 651 | } 652 | 653 | function toDateString(d) { 654 | if (d instanceof Date) { 655 | return d.toISOString(); 656 | } 657 | if (d == "now") { 658 | return new Date().toISOString(); 659 | } 660 | return d; 661 | } 662 | 663 | function formatUnit(value, unit) { 664 | let prefix = value < 0 ? -1 : 1; 665 | value = Math.abs(value); 666 | if (unit == "seconds") { 667 | if (value > 60) { 668 | return prefix*round2(value / 60) + "h"; 669 | } 670 | if (value > 1) { 671 | return prefix*round2(value) + "s"; 672 | } 673 | return prefix*round2(value * 1000) + "ms"; 674 | } 675 | if (unit == "bytes") { 676 | if (value > 1_000_000_000) { 677 | return prefix*round2(value / 1_000_000_000) + "GB"; 678 | } 679 | if (value > 1_000_000) { 680 | return prefix*round2(value / 1_000_000) + "MB"; 681 | } 682 | if (value > 1_000) { 683 | return prefix*round2(value / 1_000) + "KB"; 684 | } 685 | return prefix*round2(value) + "B"; 686 | } 687 | 688 | if (value > 1_000_000) { 689 | return prefix*round2(value / 1_000_000) + "M"; 690 | } 691 | if (value > 1_000) { 692 | return prefix*round2(value / 1_000) + "K"; 693 | } 694 | return prefix*round2(value); 695 | } 696 | 697 | function round2(value) { 698 | return Math.round(value * 100) / 100; 699 | } 700 | -------------------------------------------------------------------------------- /static/deps/js-yaml.min.js: -------------------------------------------------------------------------------- 1 | /*! js-yaml 4.0.0 https://github.com/nodeca/js-yaml @license MIT */ 2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).jsyaml={})}(this,(function(e){"use strict";function t(e){return null==e}var n={isNothing:t,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:t(e)?[]:[e]},repeat:function(e,t){var n,i="";for(n=0;nl&&(t=i-l+(o=" ... ").length),n-i>l&&(n=i+l-(a=" ...").length),{str:o+e.slice(t,n).replace(/\t/g,"→")+a,pos:i-t+o.length}}function l(e,t){return n.repeat(" ",t-e.length)+e}var c=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var i,r=/\r?\n|\r|\0/g,o=[0],c=[],s=-1;i=r.exec(e.buffer);)c.push(i.index),o.push(i.index+i[0].length),e.position<=i.index&&s<0&&(s=o.length-2);s<0&&(s=o.length-1);var u,p,f="",d=Math.min(e.line+t.linesAfter,c.length).toString().length,h=t.maxLength-(t.indent+d+3);for(u=1;u<=t.linesBefore&&!(s-u<0);u++)p=a(e.buffer,o[s-u],c[s-u],e.position-(o[s]-o[s-u]),h),f=n.repeat(" ",t.indent)+l((e.line-u+1).toString(),d)+" | "+p.str+"\n"+f;for(p=a(e.buffer,o[s],c[s],e.position,h),f+=n.repeat(" ",t.indent)+l((e.line+1).toString(),d)+" | "+p.str+"\n",f+=n.repeat("-",t.indent+d+3+p.pos)+"^\n",u=1;u<=t.linesAfter&&!(s+u>=c.length);u++)p=a(e.buffer,o[s+u],c[s+u],e.position-(o[s]-o[s+u]),h),f+=n.repeat(" ",t.indent)+l((e.line+u+1).toString(),d)+" | "+p.str+"\n";return f.replace(/\n$/,"")},s=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],u=["scalar","sequence","mapping"];var p=function(e,t){if(t=t||{},Object.keys(t).forEach((function(t){if(-1===s.indexOf(t))throw new o('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach((function(n){e[n].forEach((function(e){t[String(e)]=n}))})),t}(t.styleAliases||null),-1===u.indexOf(this.kind))throw new o('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function f(e,t,n){var i=[];return e[t].forEach((function(e){n.forEach((function(t,n){t.tag===e.tag&&t.kind===e.kind&&t.multi===e.multi&&i.push(n)})),n.push(e)})),n.filter((function(e,t){return-1===i.indexOf(t)}))}function d(e){return this.extend(e)}d.prototype.extend=function(e){var t=[],n=[];if(e instanceof p)n.push(e);else if(Array.isArray(e))n=n.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new o("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(n=n.concat(e.explicit))}t.forEach((function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new o("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new o("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),n.forEach((function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var i=Object.create(d.prototype);return i.implicit=(this.implicit||[]).concat(t),i.explicit=(this.explicit||[]).concat(n),i.compiledImplicit=f(i,"implicit",[]),i.compiledExplicit=f(i,"explicit",[]),i.compiledTypeMap=function(){var e,t,n={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function i(e){e.multi?(n.multi[e.kind].push(e),n.multi.fallback.push(e)):n[e.kind][e.tag]=n.fallback[e.tag]=e}for(e=0,t=arguments.length;e=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),w=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");var k=/^[-+]?[0-9]+e/;var C=new p("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!w.test(e)||"_"===e[e.length-1])},construct:function(e){var t,n;return n="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:n*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||n.isNegativeZero(e))},represent:function(e,t){var i;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(n.isNegativeZero(e))return"-0.0";return i=e.toString(10),k.test(i)?i.replace("e",".e"):i},defaultStyle:"lowercase"}),x=g.extend({implicit:[m,y,v,C]}),I=x,S=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),O=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");var j=new p("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(e){return null!==e&&(null!==S.exec(e)||null!==O.exec(e))},construct:function(e){var t,n,i,r,o,a,l,c,s=0,u=null;if(null===(t=S.exec(e))&&(t=O.exec(e)),null===t)throw new Error("Date resolve error");if(n=+t[1],i=+t[2]-1,r=+t[3],!t[4])return new Date(Date.UTC(n,i,r));if(o=+t[4],a=+t[5],l=+t[6],t[7]){for(s=t[7].slice(0,3);s.length<3;)s+="0";s=+s}return t[9]&&(u=6e4*(60*+t[10]+ +(t[11]||0)),"-"===t[9]&&(u=-u)),c=new Date(Date.UTC(n,i,r,o,a,l,s)),u&&c.setTime(c.getTime()-u),c},instanceOf:Date,represent:function(e){return e.toISOString()}});var T=new p("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}}),N="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";var F=new p("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,n,i=0,r=e.length,o=N;for(n=0;n64)){if(t<0)return!1;i+=6}return i%8==0},construct:function(e){var t,n,i=e.replace(/[\r\n=]/g,""),r=i.length,o=N,a=0,l=[];for(t=0;t>16&255),l.push(a>>8&255),l.push(255&a)),a=a<<6|o.indexOf(i.charAt(t));return 0===(n=r%4*6)?(l.push(a>>16&255),l.push(a>>8&255),l.push(255&a)):18===n?(l.push(a>>10&255),l.push(a>>2&255)):12===n&&l.push(a>>4&255),new Uint8Array(l)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){var t,n,i="",r=0,o=e.length,a=N;for(t=0;t>18&63],i+=a[r>>12&63],i+=a[r>>6&63],i+=a[63&r]),r=(r<<8)+e[t];return 0===(n=o%3)?(i+=a[r>>18&63],i+=a[r>>12&63],i+=a[r>>6&63],i+=a[63&r]):2===n?(i+=a[r>>10&63],i+=a[r>>4&63],i+=a[r<<2&63],i+=a[64]):1===n&&(i+=a[r>>2&63],i+=a[r<<4&63],i+=a[64],i+=a[64]),i}}),E=Object.prototype.hasOwnProperty,M=Object.prototype.toString;var L=new p("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,n,i,r,o,a=[],l=e;for(t=0,n=l.length;t>10),56320+(e-65536&1023))}for(var ee=new Array(256),te=new Array(256),ne=0;ne<256;ne++)ee[ne]=z(ne)?1:0,te[ne]=z(ne);function ie(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||q,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function re(e,t){var n={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return n.snippet=c(n),new o(t,n)}function oe(e,t){throw re(e,t)}function ae(e,t){e.onWarning&&e.onWarning.call(null,re(e,t))}var le={YAML:function(e,t,n){var i,r,o;null!==e.version&&oe(e,"duplication of %YAML directive"),1!==n.length&&oe(e,"YAML directive accepts exactly one argument"),null===(i=/^([0-9]+)\.([0-9]+)$/.exec(n[0]))&&oe(e,"ill-formed argument of the YAML directive"),r=parseInt(i[1],10),o=parseInt(i[2],10),1!==r&&oe(e,"unacceptable YAML version of the document"),e.version=n[0],e.checkLineBreaks=o<2,1!==o&&2!==o&&ae(e,"unsupported YAML version of the document")},TAG:function(e,t,n){var i,r;2!==n.length&&oe(e,"TAG directive accepts exactly two arguments"),i=n[0],r=n[1],W.test(i)||oe(e,"ill-formed tag handle (first argument) of the TAG directive"),R.call(e.tagMap,i)&&oe(e,'there is a previously declared suffix for "'+i+'" tag handle'),H.test(r)||oe(e,"ill-formed tag prefix (second argument) of the TAG directive");try{r=decodeURIComponent(r)}catch(t){oe(e,"tag prefix is malformed: "+r)}e.tagMap[i]=r}};function ce(e,t,n,i){var r,o,a,l;if(t1&&(e.result+=n.repeat("\n",t-1))}function ge(e,t){var n,i,r=e.tag,o=e.anchor,a=[],l=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=a),i=e.input.charCodeAt(e.position);0!==i&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,oe(e,"tab characters must not be used in indentation")),45===i)&&Z(e.input.charCodeAt(e.position+1));)if(l=!0,e.position++,fe(e,!0,-1)&&e.lineIndent<=t)a.push(null),i=e.input.charCodeAt(e.position);else if(n=e.line,be(e,t,3,!1,!0),a.push(e.result),fe(e,!0,-1),i=e.input.charCodeAt(e.position),(e.line===n||e.lineIndent>t)&&0!==i)oe(e,"bad indentation of a sequence entry");else if(e.lineIndentt?g=1:e.lineIndent===t?g=0:e.lineIndentt?g=1:e.lineIndent===t?g=0:e.lineIndentt)&&(y&&(a=e.line,l=e.lineStart,c=e.position),be(e,t,4,!0,r)&&(y?g=e.result:m=e.result),y||(ue(e,f,d,h,g,m,a,l,c),h=g=m=null),fe(e,!0,-1),s=e.input.charCodeAt(e.position)),(e.line===o||e.lineIndent>t)&&0!==s)oe(e,"bad indentation of a mapping entry");else if(e.lineIndent=0))break;0===o?oe(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):u?oe(e,"repeat of an indentation width identifier"):(p=t+o-1,u=!0)}if(V(a)){do{a=e.input.charCodeAt(++e.position)}while(V(a));if(35===a)do{a=e.input.charCodeAt(++e.position)}while(!G(a)&&0!==a)}for(;0!==a;){for(pe(e),e.lineIndent=0,a=e.input.charCodeAt(e.position);(!u||e.lineIndentp&&(p=e.lineIndent),G(a))f++;else{if(e.lineIndent0){for(r=a,o=0;r>0;r--)(a=Q(l=e.input.charCodeAt(++e.position)))>=0?o=(o<<4)+a:oe(e,"expected hexadecimal character");e.result+=X(o),e.position++}else oe(e,"unknown escape sequence");n=i=e.position}else G(l)?(ce(e,n,i,!0),he(e,fe(e,!1,t)),n=i=e.position):e.position===e.lineStart&&de(e)?oe(e,"unexpected end of the document within a double quoted scalar"):(e.position++,i=e.position)}oe(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?y=!0:!function(e){var t,n,i;if(42!==(i=e.input.charCodeAt(e.position)))return!1;for(i=e.input.charCodeAt(++e.position),t=e.position;0!==i&&!Z(i)&&!J(i);)i=e.input.charCodeAt(++e.position);return e.position===t&&oe(e,"name of an alias node must contain at least one character"),n=e.input.slice(t,e.position),R.call(e.anchorMap,n)||oe(e,'unidentified alias "'+n+'"'),e.result=e.anchorMap[n],fe(e,!0,-1),!0}(e)?function(e,t,n){var i,r,o,a,l,c,s,u,p=e.kind,f=e.result;if(Z(u=e.input.charCodeAt(e.position))||J(u)||35===u||38===u||42===u||33===u||124===u||62===u||39===u||34===u||37===u||64===u||96===u)return!1;if((63===u||45===u)&&(Z(i=e.input.charCodeAt(e.position+1))||n&&J(i)))return!1;for(e.kind="scalar",e.result="",r=o=e.position,a=!1;0!==u;){if(58===u){if(Z(i=e.input.charCodeAt(e.position+1))||n&&J(i))break}else if(35===u){if(Z(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&de(e)||n&&J(u))break;if(G(u)){if(l=e.line,c=e.lineStart,s=e.lineIndent,fe(e,!1,-1),e.lineIndent>=t){a=!0,u=e.input.charCodeAt(e.position);continue}e.position=o,e.line=l,e.lineStart=c,e.lineIndent=s;break}}a&&(ce(e,r,o,!1),he(e,e.line-l),r=o=e.position,a=!1),V(u)||(o=e.position+1),u=e.input.charCodeAt(++e.position)}return ce(e,r,o,!1),!!e.result||(e.kind=p,e.result=f,!1)}(e,d,1===i)&&(y=!0,null===e.tag&&(e.tag="?")):(y=!0,null===e.tag&&null===e.anchor||oe(e,"alias node should not have any properties")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===g&&(y=c&&ge(e,h))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&oe(e,'unacceptable node kind for ! tag; it should be "scalar", not "'+e.kind+'"'),s=0,u=e.implicitTypes.length;s"),null!==e.result&&f.kind!==e.kind&&oe(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+f.kind+'", not "'+e.kind+'"'),f.resolve(e.result,e.tag)?(e.result=f.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):oe(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||y}function Ae(e){var t,n,i,r,o=e.position,a=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(r=e.input.charCodeAt(e.position))&&(fe(e,!0,-1),r=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==r));){for(a=!0,r=e.input.charCodeAt(++e.position),t=e.position;0!==r&&!Z(r);)r=e.input.charCodeAt(++e.position);for(i=[],(n=e.input.slice(t,e.position)).length<1&&oe(e,"directive name must not be less than one character in length");0!==r;){for(;V(r);)r=e.input.charCodeAt(++e.position);if(35===r){do{r=e.input.charCodeAt(++e.position)}while(0!==r&&!G(r));break}if(G(r))break;for(t=e.position;0!==r&&!Z(r);)r=e.input.charCodeAt(++e.position);i.push(e.input.slice(t,e.position))}0!==r&&pe(e),R.call(le,n)?le[n](e,n,i):ae(e,'unknown document directive "'+n+'"')}fe(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,fe(e,!0,-1)):a&&oe(e,"directives end mark is expected"),be(e,e.lineIndent-1,4,!1,!0),fe(e,!0,-1),e.checkLineBreaks&&K.test(e.input.slice(o,e.position))&&ae(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&de(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,fe(e,!0,-1)):e.position=55296&&i<=56319&&t+1=56320&&n<=57343?1024*(i-55296)+n-56320+65536:i}function Ue(e){return/^\n* /.test(e)}function Ye(e,t,n,i,r,o,a,l){var c,s,u=0,p=null,f=!1,d=!1,h=-1!==i,g=-1,m=Me(s=De(e,0))&&s!==xe&&!Ee(s)&&45!==s&&63!==s&&58!==s&&44!==s&&91!==s&&93!==s&&123!==s&&125!==s&&35!==s&&38!==s&&42!==s&&33!==s&&124!==s&&61!==s&&62!==s&&39!==s&&34!==s&&37!==s&&64!==s&&96!==s&&function(e){return!Ee(e)&&58!==e}(De(e,e.length-1));if(t||a)for(c=0;c=65536?c+=2:c++){if(!Me(u=De(e,c)))return 5;m=m&&_e(u,p,l),p=u}else{for(c=0;c=65536?c+=2:c++){if(10===(u=De(e,c)))f=!0,h&&(d=d||c-g-1>i&&" "!==e[g+1],g=c);else if(!Me(u))return 5;m=m&&_e(u,p,l),p=u}d=d||h&&c-g-1>i&&" "!==e[g+1]}return f||d?n>9&&Ue(e)?5:a?2===o?5:2:d?4:3:!m||a||r(e)?2===o?5:2:1}function qe(e,t,n,i,r){e.dump=function(){if(0===t.length)return 2===e.quotingType?'""':"''";if(!e.noCompatMode&&(-1!==Se.indexOf(t)||Oe.test(t)))return 2===e.quotingType?'"'+t+'"':"'"+t+"'";var a=e.indent*Math.max(1,n),l=-1===e.lineWidth?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-a),c=i||e.flowLevel>-1&&n>=e.flowLevel;switch(Ye(t,c,e.indent,l,(function(t){return function(e,t){var n,i;for(n=0,i=e.implicitTypes.length;n"+Re(t,e.indent)+Be(Ne(function(e,t){var n,i,r=/(\n+)([^\n]*)/g,o=(l=e.indexOf("\n"),l=-1!==l?l:e.length,r.lastIndex=l,Ke(e.slice(0,l),t)),a="\n"===e[0]||" "===e[0];var l;for(;i=r.exec(e);){var c=i[1],s=i[2];n=" "===s[0],o+=c+(a||n||""===s?"":"\n")+Ke(s,t),a=n}return o}(t,l),a));case 5:return'"'+function(e){for(var t,n="",i=0,r=0;r=65536?r+=2:r++)i=De(e,r),!(t=Ie[i])&&Me(i)?(n+=e[r],i>=65536&&(n+=e[r+1])):n+=t||je(i);return n}(t)+'"';default:throw new o("impossible error: invalid scalar style")}}()}function Re(e,t){var n=Ue(e)?String(t):"",i="\n"===e[e.length-1];return n+(i&&("\n"===e[e.length-2]||"\n"===e)?"+":i?"":"-")+"\n"}function Be(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function Ke(e,t){if(""===e||" "===e[0])return e;for(var n,i,r=/ [^ ]/g,o=0,a=0,l=0,c="";n=r.exec(e);)(l=n.index)-o>t&&(i=a>o?a:l,c+="\n"+e.slice(o,i),o=i+1),a=l;return c+="\n",e.length-o>t&&a>o?c+=e.slice(o,a)+"\n"+e.slice(a+1):c+=e.slice(o),c.slice(1)}function Pe(e,t,n,i){var r,o,a,l="",c=e.tag;for(r=0,o=n.length;r tag resolver accepts not "'+s+'" style');i=c.represent[s](t,s)}e.dump=i}return!0}return!1}function He(e,t,n,i,r,a,l){e.tag=null,e.dump=n,We(e,n,!1)||We(e,n,!0);var c,s=ke.call(e.dump),u=i;i&&(i=e.flowLevel<0||e.flowLevel>t);var p,f,d="[object Object]"===s||"[object Array]"===s;if(d&&(f=-1!==(p=e.duplicates.indexOf(n))),(null!==e.tag&&"?"!==e.tag||f||2!==e.indent&&t>0)&&(r=!1),f&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(d&&f&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),"[object Object]"===s)i&&0!==Object.keys(e.dump).length?(!function(e,t,n,i){var r,a,l,c,s,u,p="",f=e.tag,d=Object.keys(n);if(!0===e.sortKeys)d.sort();else if("function"==typeof e.sortKeys)d.sort(e.sortKeys);else if(e.sortKeys)throw new o("sortKeys must be a boolean or a function");for(r=0,a=d.length;r1024)&&(e.dump&&10===e.dump.charCodeAt(0)?u+="?":u+="? "),u+=e.dump,s&&(u+=Fe(e,t)),He(e,t+1,c,!0,s)&&(e.dump&&10===e.dump.charCodeAt(0)?u+=":":u+=": ",p+=u+=e.dump));e.tag=f,e.dump=p||"{}"}(e,t,e.dump,r),f&&(e.dump="&ref_"+p+e.dump)):(!function(e,t,n){var i,r,o,a,l,c="",s=e.tag,u=Object.keys(n);for(i=0,r=u.length;i1024&&(l+="? "),l+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),He(e,t,a,!1,!1)&&(c+=l+=e.dump));e.tag=s,e.dump="{"+c+"}"}(e,t,e.dump),f&&(e.dump="&ref_"+p+" "+e.dump));else if("[object Array]"===s)i&&0!==e.dump.length?(e.noArrayIndent&&!l&&t>0?Pe(e,t-1,e.dump,r):Pe(e,t,e.dump,r),f&&(e.dump="&ref_"+p+e.dump)):(!function(e,t,n){var i,r,o,a="",l=e.tag;for(i=0,r=n.length;i",e.dump=c+" "+e.dump)}return!0}function $e(e,t){var n,i,r=[],o=[];for(Ge(e,r,o),n=0,i=o.length;n