├── .github └── workflows │ └── deploy.yaml ├── .gitignore ├── CNAME ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── book.toml ├── docs ├── .vitepress │ ├── config.js │ └── theme │ │ ├── index.js │ │ └── styles │ │ └── mathjax3.css ├── index.md └── observability │ ├── metrics.md │ ├── metrics │ ├── instrumenting_go │ │ ├── echo.md │ │ ├── gin.md │ │ ├── intro.md │ │ ├── middlewares.md │ │ └── simple_service.md │ ├── labels.md │ ├── types.md │ └── types │ │ ├── counters.md │ │ ├── gauges.md │ │ ├── histograms.md │ │ └── images │ │ ├── counter_interpolation.png │ │ ├── counter_prom_1.png │ │ ├── counter_prom_2.png │ │ ├── counter_prom_3.png │ │ ├── counter_prom_4.png │ │ ├── counter_prom_5.png │ │ ├── counter_prom_6.png │ │ ├── counter_prom_7.png │ │ └── gauge_interpolation.png │ └── observability.md ├── package.json └── yarn.lock /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-20.04 12 | permissions: 13 | contents: "read" 14 | id-token: "write" 15 | pages: "write" 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | cache: yarn 24 | - run: yarn install --frozen-lockfile 25 | - name: Build 26 | run: yarn docs:build 27 | - name: Setup Pages 28 | uses: actions/configure-pages@v2 29 | - name: Upload artifact 30 | uses: actions/upload-pages-artifact@v1 31 | with: 32 | # Upload compiled book 33 | path: docs/.vitepress/dist 34 | - name: Deploy to GitHub Pages 35 | id: deployment 36 | uses: actions/deploy-pages@v1 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | node_modules/ 3 | docs/.vitepress/cache/ 4 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | book.ericv.me -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.14-alpine 2 | 3 | RUN mkdir /docs 4 | 5 | RUN apk add git 6 | 7 | WORKDIR /docs 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric Volpert 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 | This book should not be published physically and/or in commercial form without explicit 16 | permission from the author (Eric Volpert). 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/usr/bin/env bash 2 | COMPOSE_RUN_NODE = docker-compose run --rm node 3 | COMPOSE_UP_NODE = docker-compose up -d node 4 | COMPOSE_UP_NODE_DEV = docker-compose up node_dev 5 | SERVE_BASE_URL ?= http://node:5173 6 | .DEFAULT_GOAL := help 7 | help: 8 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 9 | 10 | build: ## Run mdbook to build content 11 | docker run --rm -v $(PWD):/book peaceiris/mdbook:v0.4.21 build 12 | 13 | # dev: ## Run mdbook locally and serve content at http://localhost:3000/ with live reloading 14 | # docker run --rm -it -v $(PWD):/book -p 3000:3000 peaceiris/mdbook:v0.4.21 serve -n 0.0.0.0 15 | 16 | dev: 17 | docker build . -t book 18 | docker run --rm -it -v $(PWD):/docs -p 5173:5173 book yarn docs:dev --host 0.0.0.0 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Practical Observability 2 | 3 | A book that covers the application of observability practices. 4 | 5 | We cover the topics of Metrics, Tracing, and Logging, as well as how common observability providers implement each of these ecosystems. 6 | 7 | You will learn the theory behind different kinds of observability tools, how they are implemented from first principles, and how to instrument new and existing services in a few popular programming languages (Golang and Python). 8 | 9 | ## Deployment 10 | 11 | This book is deployed as a draft in GCS for now: 12 | 13 | | **Environment** | **Book Link** | 14 | | --------------- | ------------------------------------------------------------------------------------ | 15 | | GCS | [Practical Observability](https://storage.googleapis.com/ericv-o11y-book/index.html) | 16 | | GH Pages | [Practical Observability](https://book.ericv.me/) | 17 | 18 | ## Development 19 | 20 | To run this project locally, simply clone the repository and run `make dev` to bring up a `mdbook` container that exposes the docs at `http://localhost:3000` 21 | 22 | You'll need a recent version of Docker in order to run the project. 23 | 24 | ### Demo Stack Standup 25 | 26 | To startup the local demo stack including Prometheus, Grafana, and AlertManager, use the following steps on your local Kubernetes cluster. 27 | 28 | Install Helm 29 | 30 | ```shell 31 | $ brew install helm 32 | ``` 33 | 34 | Install the Helm Prometheus Repo and Charts 35 | 36 | ```shell 37 | $ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 38 | $ helm repo add stable https://charts.helm.sh/stable 39 | $ helm repo update 40 | ``` 41 | 42 | Create a Monitoring Namespace for Prometheus components 43 | 44 | ```shell 45 | $ kubectl create namespace monitoring 46 | ``` 47 | 48 | Run the Prometheus Helm Chart 49 | 50 | Note the ports we're assigning to each service, these can be changed if necessary to accomodate your local environment. 51 | 52 | ```shell 53 | $ helm install kind-prometheus prometheus-community/kube-prometheus-stack \ 54 | --namespace monitoring \ 55 | --set prometheus.service.nodePort=30000 \ 56 | --set prometheus.service.type=NodePort \ 57 | --set grafana.service.nodePort=31000 \ 58 | --set grafana.service.type=NodePort \ 59 | --set alertmanager.service.nodePort=32000 \ 60 | --set alertmanager.service.type=NodePort \ 61 | --set prometheus-node-exporter.service.nodePort=32001 \ 62 | --set prometheus-node-exporter.service.type=NodePort 63 | ``` 64 | 65 | Patch the Node Exporter to keep it from crashing since it has difficulties with Docker Desktop clusters on occasion: 66 | 67 | ```shell 68 | $ kubectl patch -n monitoring ds kind-prometheus-prometheus-node-exporter --type "json" -p '[{"op": "remove", "path" : "/spec/template/spec/containers/0/volumeMounts/2/mountPropagation"}]' 69 | ``` 70 | 71 | Check for running Prometheus pods: 72 | 73 | ```shell 74 | $ kubectl --namespace monitoring get pods -l release=kind-prometheus 75 | NAME READY STATUS RESTARTS AGE 76 | kind-prometheus-kube-prome-operator-75468846f9-ng4kk 1/1 Running 0 6m14s 77 | kind-prometheus-kube-state-metrics-554c667875-mg27l 1/1 Running 0 6m14s 78 | kind-prometheus-prometheus-node-exporter-l7qng 1/1 Running 0 55s 79 | ``` 80 | 81 | Check the full component stack: 82 | 83 | ```shell 84 | $ kubectl get all --namespace monitoring 85 | 86 | NAME READY STATUS RESTARTS AGE 87 | pod/alertmanager-kind-prometheus-kube-prome-alertmanager-0 2/2 Running 1 (7m21s ago) 7m43s 88 | pod/kind-prometheus-grafana-59764d785-fq26p 3/3 Running 0 7m59s 89 | pod/kind-prometheus-kube-prome-operator-75468846f9-ng4kk 1/1 Running 0 7m59s 90 | pod/kind-prometheus-kube-state-metrics-554c667875-mg27l 1/1 Running 0 7m59s 91 | pod/kind-prometheus-prometheus-node-exporter-l7qng 1/1 Running 0 2m40s 92 | pod/prometheus-kind-prometheus-kube-prome-prometheus-0 2/2 Running 0 7m43s 93 | 94 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 95 | service/alertmanager-operated ClusterIP None 9093/TCP,9094/TCP,9094/UDP 7m43s 96 | service/kind-prometheus-grafana NodePort 10.101.226.52 80:31000/TCP 7m59s 97 | service/kind-prometheus-kube-prome-alertmanager NodePort 10.109.27.17 9093:32000/TCP 7m59s 98 | service/kind-prometheus-kube-prome-operator ClusterIP 10.96.108.97 443/TCP 7m59s 99 | service/kind-prometheus-kube-prome-prometheus NodePort 10.105.140.103 9090:30000/TCP 7m59s 100 | service/kind-prometheus-kube-state-metrics ClusterIP 10.111.190.206 8080/TCP 7m59s 101 | service/kind-prometheus-prometheus-node-exporter NodePort 10.99.3.90 9100:32001/TCP 7m59s 102 | service/prometheus-operated ClusterIP None 9090/TCP 7m43s 103 | 104 | NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE 105 | daemonset.apps/kind-prometheus-prometheus-node-exporter 1 1 1 1 1 7m59s 106 | 107 | NAME READY UP-TO-DATE AVAILABLE AGE 108 | deployment.apps/kind-prometheus-grafana 1/1 1 1 7m59s 109 | deployment.apps/kind-prometheus-kube-prome-operator 1/1 1 1 7m59s 110 | deployment.apps/kind-prometheus-kube-state-metrics 1/1 1 1 7m59s 111 | 112 | NAME DESIRED CURRENT READY AGE 113 | replicaset.apps/kind-prometheus-grafana-59764d785 1 1 1 7m59s 114 | replicaset.apps/kind-prometheus-kube-prome-operator-75468846f9 1 1 1 7m59s 115 | replicaset.apps/kind-prometheus-kube-state-metrics-554c667875 1 1 1 7m59s 116 | 117 | NAME READY AGE 118 | statefulset.apps/alertmanager-kind-prometheus-kube-prome-alertmanager 1/1 7m43s 119 | statefulset.apps/prometheus-kind-prometheus-kube-prome-prometheus 1/1 7m43s 120 | ``` 121 | 122 | After the install you'll find: 123 | 124 | - Prometheus at [`http://localhost:30000`](http://localhost:30000) 125 | - Grafana at [`http://localhost:31000`](http://localhost:31000) 126 | - AlertManager at [`http://localhost:32000`](http://localhost:32000) 127 | 128 | Log into Grafana with the following credentials: 129 | 130 | ``` 131 | Username: admin 132 | Password: prom-operator 133 | ``` 134 | 135 | ### Teardown 136 | 137 | Teardown the stack when you're done with: 138 | 139 | ```shell 140 | $ kubectl delete namespace monitoring 141 | ``` 142 | 143 | ## CI/CD 144 | 145 | This repo has one main CI pipeline, that builds and publishes the draft to GCS in a GitHub Action. 146 | 147 | Commits to the main branch will trigger a build and deploy and generally within 20 seconds of a push you should see the updated docs at the `Draft` environment link. 148 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Eric Volpert"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Practical Observability" 7 | 8 | [output.html] 9 | mathjax-support = true 10 | no-section-label = true 11 | default-theme = "ayu" 12 | preferred-dark-theme = "ayu" 13 | 14 | [output.html.fold] 15 | enable = true # whether or not to enable section folding 16 | level = 2 # the depth to start folding 17 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('vitepress').UserConfig} 3 | */ 4 | 5 | import mathjax3 from 'markdown-it-mathjax3'; 6 | 7 | const domain = 'book.ericv.me' 8 | const url = `https://${domain}` 9 | const desc = 'A book that covers topics around observability in modern software systems' 10 | const title = 'Practical Observability' 11 | const github = 'https://github.com/ericovlp12/practical-observability' 12 | 13 | // Custom Elements for MathJax 14 | const customElements = [ 15 | 'mjx-container', 16 | 'mjx-assistive-mml', 17 | 'math', 18 | 'maction', 19 | 'maligngroup', 20 | 'malignmark', 21 | 'menclose', 22 | 'merror', 23 | 'mfenced', 24 | 'mfrac', 25 | 'mi', 26 | 'mlongdiv', 27 | 'mmultiscripts', 28 | 'mn', 29 | 'mo', 30 | 'mover', 31 | 'mpadded', 32 | 'mphantom', 33 | 'mroot', 34 | 'mrow', 35 | 'ms', 36 | 'mscarries', 37 | 'mscarry', 38 | 'mscarries', 39 | 'msgroup', 40 | 'mstack', 41 | 'mlongdiv', 42 | 'msline', 43 | 'mstack', 44 | 'mspace', 45 | 'msqrt', 46 | 'msrow', 47 | 'mstack', 48 | 'mstack', 49 | 'mstyle', 50 | 'msub', 51 | 'msup', 52 | 'msubsup', 53 | 'mtable', 54 | 'mtd', 55 | 'mtext', 56 | 'mtr', 57 | 'munder', 58 | 'munderover', 59 | 'semantics', 60 | 'math', 61 | 'mi', 62 | 'mn', 63 | 'mo', 64 | 'ms', 65 | 'mspace', 66 | 'mtext', 67 | 'menclose', 68 | 'merror', 69 | 'mfenced', 70 | 'mfrac', 71 | 'mpadded', 72 | 'mphantom', 73 | 'mroot', 74 | 'mrow', 75 | 'msqrt', 76 | 'mstyle', 77 | 'mmultiscripts', 78 | 'mover', 79 | 'mprescripts', 80 | 'msub', 81 | 'msubsup', 82 | 'msup', 83 | 'munder', 84 | 'munderover', 85 | 'none', 86 | 'maligngroup', 87 | 'malignmark', 88 | 'mtable', 89 | 'mtd', 90 | 'mtr', 91 | 'mlongdiv', 92 | 'mscarries', 93 | 'mscarry', 94 | 'msgroup', 95 | 'msline', 96 | 'msrow', 97 | 'mstack', 98 | 'maction', 99 | 'semantics', 100 | 'annotation', 101 | 'annotation-xml', 102 | ]; 103 | 104 | export default { 105 | title: title, 106 | description: desc, 107 | lastUpdated: true, 108 | 109 | head: [ 110 | // ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon_io/apple-touch-icon.png' }], 111 | // ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon_io/favicon-32x32.png' }], 112 | // ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon_io/favicon-16x16.png' }], 113 | // ['link', { rel: 'manifest', href: 'favicon_io/site.webmanifest' }], 114 | 115 | // Open graph protocol 116 | ['meta', { property: 'og:url', content: url }], 117 | ['meta', { property: 'og:title', content: title }], 118 | ['meta', { property: 'og:description', content: desc }], 119 | ['meta', { property: 'og:site_name', content: domain }], 120 | 121 | //twitter card tags additive with the og: tags 122 | ['meta', { name: 'twitter:domain', value: domain }], 123 | ['meta', { name: 'twitter:url', value: url }], 124 | ], 125 | 126 | markdown: { 127 | config: (md) => { 128 | md.use(require('markdown-it-footnote')); 129 | md.use(mathjax3); 130 | } 131 | }, 132 | 133 | themeConfig: { 134 | siteTitle: title, 135 | outline: [2, 6], 136 | nav: [], 137 | socialLinks: [ 138 | { icon: 'github', link: github }, 139 | ], 140 | sidebar: [ 141 | { 142 | text: 'Observability', 143 | link: '/observability/observability', 144 | items: [ 145 | { 146 | text: 'Metrics', link: '/observability/metrics', items: [ 147 | { 148 | text: 'Labels', link: '/observability/metrics/labels' 149 | }, 150 | { 151 | text: 'Types', link: '/observability/metrics/types', items: [ 152 | { 153 | text: 'Counters', link: '/observability/metrics/types/counters' 154 | }, 155 | { 156 | text: 'Gauges', link: '/observability/metrics/types/gauges' 157 | }, 158 | { 159 | text: 'Histograms', link: '/observability/metrics/types/histograms' 160 | }, 161 | ] 162 | }, 163 | { 164 | text: 'Instrumenting Go', link: '/observability/metrics/instrumenting_go/intro', items: [ 165 | { 166 | text: 'Simple Service', link: '/observability/metrics/instrumenting_go/simple_service' 167 | }, 168 | { 169 | text: 'Middleware', link: '/observability/metrics/instrumenting_go/middlewares' 170 | }, 171 | { 172 | text: 'Gin', link: '/observability/metrics/instrumenting_go/gin' 173 | }, 174 | { 175 | text: 'Echo', link: '/observability/metrics/instrumenting_go/echo' 176 | } 177 | ] 178 | } 179 | ] 180 | } 181 | ] 182 | }, 183 | ] 184 | }, 185 | 186 | vue: { 187 | template: { 188 | compilerOptions: { 189 | isCustomElement: (tag) => customElements.includes(tag), 190 | }, 191 | }, 192 | }, 193 | } 194 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import './styles/mathjax3.css'; 2 | 3 | export { default } from 'vitepress/theme'; 4 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/mathjax3.css: -------------------------------------------------------------------------------- 1 | mjx-container { 2 | display: inline-block; 3 | margin: auto 2px -2px; 4 | } 5 | 6 | mjx-container > svg { 7 | margin: auto; 8 | } 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | titleTemplate: Welcome to Practical Observability 4 | hero: 5 | name: Practical Observability 6 | tagline: Applied observability in modern software systems 7 | actions: 8 | - text: Get Started 9 | link: /observability/observability 10 | theme: brand 11 | --- 12 | -------------------------------------------------------------------------------- /docs/observability/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | ## What Metrics are Not 4 | 5 | Metrics are not a silver bullet to solving all your production issues. In fact, in a world of microservices where each service talks to a number of other services in order to serve a request, metrics can be deceptive, difficult to understand, and are often missing where we need them most because we failed to anticipate what we should have been measuring. 6 | 7 | > In a modern world, debugging with metrics requires you to connect dozens of disconnected metrics that were recorded over the course of executing any one particular request, across any number of services or machines, to infer what might have occurred over the various hops needed for its fulfillment. The helpfulness of those dozens of clues depends entirely upon whether someone was able to predict, in advance, if that measurement was over or under the threshold that meant this action contributed to creating a previously unknown anomalous failure mode that had never been previously encountered.[^1] 8 | 9 | That being said, metrics can be useful in the process of debugging and triaging production incidents. Metrics provide a helpful summary of the state of a system, and while they may not hold all the answers, they allow us to ask more specific questions which help to guide investigations. Additionally, metrics can be the basis for alerts which provide on-call engineers with early warnings, helping us catch incidents before they impact users. 10 | 11 | ## What Metrics Are 12 | 13 | Metrics are numerical time-series data that describe the state of systems over time. 14 | 15 | Common base metrics include the following: 16 | - Latency between receiving a HTTP request and sending a response 17 | - CPU seconds being utilized by a system 18 | - Number of requests being processed by a system 19 | - Size of response bodies being sent by a system 20 | - Number of requests that result in errors being sent by a system 21 | 22 | These base metrics are all things we can measure directly in a given system. They don't immediately seem useful in determining if something is wrong with the system but from these core metrics we can _derive_ higher level metrics that indicate when systems may be unhealthy. 23 | 24 | Common derived metrics include: 25 | - 95th Percentile of HTTP Request/Response Latency over the past 5 minutes 26 | - Average and Maximum CPU Utilization % across all deployed containers for a system over the past hour 27 | - Request throughput (\\( \frac{req}{s}\\)) being handled by a system over the past 5 minutes 28 | - Ingress and Egress Bandwidth (\\( \frac{MB}{s}\\)) of a system over the past hour 29 | - Error (\\(\frac{failed\ requests}{total\ requests}\\)) rate (\\( \frac{error}{s}\\)) of a system over the past 5 minutes 30 | 31 | Derived metrics, when graphed, make it easy to visually pattern match to find outliers in system operation. Derived metrics can also be utilized for alerting when outside of "normal" operating ranges, but generally don't determine directly that there is something wrong with the system. 32 | 33 | As engineers, we track base metrics in our systems and expose them for collection regularly so that our observability platform can record the change over time of these base metrics. We pick base metrics that provide measurable truth of the state of the system using absolute values where possible. With a large enough pool of base metrics, we can generate dozens of derived metrics that interpret the base metrics _in context_, making it clear to the viewer both _what_ is being measured and _why_ it is important. 34 | 35 | [^1]: Lots of content for these docs is inspired and sometimes directly drawn from Charity Majors, Liz Fong-Jones, and George Miranda's book, ["Observability Engineering"](https://info.honeycomb.io/observability-engineering-oreilly-book-2022) 36 | 37 | -------------------------------------------------------------------------------- /docs/observability/metrics/instrumenting_go/echo.md: -------------------------------------------------------------------------------- 1 | # Instrumenting an Echo App 2 | 3 | Let's build the example service again, this time with Echo: 4 | 5 | ## Example Echo Service 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "net/http" 12 | 13 | "github.com/labstack/echo/v4" 14 | ) 15 | 16 | // User is a struct representing a user object 17 | type User struct { 18 | ID int `json:"id"` 19 | Name string `json:"name"` 20 | } 21 | 22 | var users []User 23 | 24 | func main() { 25 | e := echo.New() 26 | e.GET("/users", listUsers) 27 | e.POST("/users", createUser) 28 | e.Start(":8080") 29 | } 30 | 31 | func listUsers(c echo.Context) error { 32 | // List all users 33 | return c.JSON(http.StatusOK, users) 34 | } 35 | 36 | func createUser(c echo.Context) error { 37 | // Create a new user 38 | var user User 39 | if err := c.Bind(&user); err != nil { 40 | return err 41 | } 42 | users = append(users, user) 43 | return c.JSON(http.StatusOK, user) 44 | } 45 | ``` 46 | 47 | This time around the only thing we need to be wary of is that Echo uses the `Content-Type` header on requests in the `c.Bind()` method, so if we don't specify that our payload with the `Content-Type: application/json` header, the `c.Bind()` method will return an empty `User` object and add that to the user list. 48 | 49 | ## Instrumenting Echo with Prometheus Middleware 50 | 51 | Echo has a standard [Prometheus Instrumentation Middleware](https://echo.labstack.com/middleware/prometheus/) included in its `contrib` library that we can add to our existing application. 52 | 53 | Import the middleware library from `echo-contrib`: 54 | 55 | ```go 56 | import ( 57 | "net/http" 58 | 59 | "github.com/labstack/echo-contrib/prometheus" 60 | "github.com/labstack/echo/v4" 61 | ) 62 | ``` 63 | 64 | Then enable the metrics middleware inside the `main()` func: 65 | 66 | ```go 67 | func main() { 68 | e := echo.New() 69 | // Enable metrics middleware 70 | p := prometheus.NewPrometheus("echo", nil) 71 | p.Use(e) 72 | e.GET("/users", listUsers) 73 | e.POST("/users", createUser) 74 | e.Start(":8080") 75 | } 76 | ``` 77 | 78 | We can start up our server listening on port `8080` with: 79 | 80 | ```shell 81 | go run main.go 82 | ``` 83 | 84 | Let's run our suite of `curl` requests (slightly modified to include `Content-Type` headers) and see what the `/metrics` endpoint has for us: 85 | 86 | ```shell 87 | $ curl http://localhost:8080/users 88 | > null 89 | $ curl -X POST -d'{"name":"Eric","id":1}' \ 90 | -H 'Content-Type: application/json' \ 91 | http://localhost:8080/users 92 | > {"id":1,"name":"Eric"} 93 | $ curl http://localhost:8080/users 94 | > [{"id":1,"name":"Eric"}] 95 | $ curl -X POST -d'{"name":' \ 96 | -H 'Content-Type: application/json' \ 97 | http://localhost:8080/users 98 | > {"message":"unexpected EOF"} 99 | ``` 100 | 101 | With some data in place, we can `GET` the `/metrics` endpoint to see our histograms. 102 | 103 | I'll collapse the results below because Echo generates lots of histograms by default and with just our four requests we have > 130 lines of metrics. 104 | 105 |
106 | 107 | Metrics Response 108 | 109 | 110 | ```shell 111 | $ curl http://localhost:8080/metrics 112 | > # HELP echo_request_duration_seconds The HTTP request latencies in seconds. 113 | > # TYPE echo_request_duration_seconds histogram 114 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.005"} 2 115 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.01"} 2 116 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.025"} 2 117 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.05"} 2 118 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.1"} 2 119 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.25"} 2 120 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.5"} 2 121 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="1"} 2 122 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="2.5"} 2 123 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="5"} 2 124 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="10"} 2 125 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="+Inf"} 2 126 | > echo_request_duration_seconds_sum{code="200",method="GET",url="/users"} 0.00010224 127 | > echo_request_duration_seconds_count{code="200",method="GET",url="/users"} 2 128 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.005"} 1 129 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.01"} 1 130 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.025"} 1 131 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.05"} 1 132 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.1"} 1 133 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.25"} 1 134 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.5"} 1 135 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="1"} 1 136 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="2.5"} 1 137 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="5"} 1 138 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="10"} 1 139 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="+Inf"} 1 140 | > echo_request_duration_seconds_sum{code="200",method="POST",url="/users"} 9.14e-05 141 | > echo_request_duration_seconds_count{code="200",method="POST",url="/users"} 1 142 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.005"} 1 143 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.01"} 1 144 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.025"} 1 145 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.05"} 1 146 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.1"} 1 147 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.25"} 1 148 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.5"} 1 149 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="1"} 1 150 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="2.5"} 1 151 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="5"} 1 152 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="10"} 1 153 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="+Inf"} 1 154 | > echo_request_duration_seconds_sum{code="400",method="POST",url="/users"} 4.864e-05 155 | > echo_request_duration_seconds_count{code="400",method="POST",url="/users"} 1 156 | > # HELP echo_request_size_bytes The HTTP request sizes in bytes. 157 | > # TYPE echo_request_size_bytes histogram 158 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="1024"} 2 159 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="2048"} 2 160 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="5120"} 2 161 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="10240"} 2 162 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="102400"} 2 163 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="512000"} 2 164 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="1.048576e+06"} 2 165 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="2.62144e+06"} 2 166 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="5.24288e+06"} 2 167 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="1.048576e+07"} 2 168 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="+Inf"} 2 169 | > echo_request_size_bytes_sum{code="200",method="GET",url="/users"} 122 170 | > echo_request_size_bytes_count{code="200",method="GET",url="/users"} 2 171 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="1024"} 1 172 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="2048"} 1 173 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="5120"} 1 174 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="10240"} 1 175 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="102400"} 1 176 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="512000"} 1 177 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="1.048576e+06"} 1 178 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="2.62144e+06"} 1 179 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="5.24288e+06"} 1 180 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="1.048576e+07"} 1 181 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="+Inf"} 1 182 | > echo_request_size_bytes_sum{code="200",method="POST",url="/users"} 128 183 | > echo_request_size_bytes_count{code="200",method="POST",url="/users"} 1 184 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="1024"} 1 185 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="2048"} 1 186 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="5120"} 1 187 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="10240"} 1 188 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="102400"} 1 189 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="512000"} 1 190 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="1.048576e+06"} 1 191 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="2.62144e+06"} 1 192 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="5.24288e+06"} 1 193 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="1.048576e+07"} 1 194 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="+Inf"} 1 195 | > echo_request_size_bytes_sum{code="400",method="POST",url="/users"} 113 196 | > echo_request_size_bytes_count{code="400",method="POST",url="/users"} 1 197 | > # HELP echo_requests_total How many HTTP requests processed, partitioned by status code and HTTP method. 198 | > # TYPE echo_requests_total counter 199 | > echo_requests_total{code="200",host="localhost:8080",method="GET",url="/users"} 2 200 | > echo_requests_total{code="200",host="localhost:8080",method="POST",url="/users"} 1 201 | > echo_requests_total{code="400",host="localhost:8080",method="POST",url="/users"} 1 202 | > # HELP echo_response_size_bytes The HTTP response sizes in bytes. 203 | > # TYPE echo_response_size_bytes histogram 204 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="1024"} 2 205 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="2048"} 2 206 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="5120"} 2 207 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="10240"} 2 208 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="102400"} 2 209 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="512000"} 2 210 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="1.048576e+06"} 2 211 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="2.62144e+06"} 2 212 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="5.24288e+06"} 2 213 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="1.048576e+07"} 2 214 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="+Inf"} 2 215 | > echo_response_size_bytes_sum{code="200",method="GET",url="/users"} 30 216 | > echo_response_size_bytes_count{code="200",method="GET",url="/users"} 2 217 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="1024"} 1 218 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="2048"} 1 219 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="5120"} 1 220 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="10240"} 1 221 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="102400"} 1 222 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="512000"} 1 223 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="1.048576e+06"} 1 224 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="2.62144e+06"} 1 225 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="5.24288e+06"} 1 226 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="1.048576e+07"} 1 227 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="+Inf"} 1 228 | > echo_response_size_bytes_sum{code="200",method="POST",url="/users"} 23 229 | > echo_response_size_bytes_count{code="200",method="POST",url="/users"} 1 230 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="1024"} 1 231 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="2048"} 1 232 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="5120"} 1 233 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="10240"} 1 234 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="102400"} 1 235 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="512000"} 1 236 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="1.048576e+06"} 1 237 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="2.62144e+06"} 1 238 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="5.24288e+06"} 1 239 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="1.048576e+07"} 1 240 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="+Inf"} 1 241 | > echo_response_size_bytes_sum{code="400",method="POST",url="/users"} 0 242 | > echo_response_size_bytes_count{code="400",method="POST",url="/users"} 1 243 | ``` 244 | 245 |
246 | 247 | ## Breaking Down Echo's Metrics 248 | 249 | Let's break down the histograms we got back from Echo's Prometheus middleware: 250 | 251 | ### Response Time Histograms 252 | 253 | ```shell 254 | > # HELP echo_request_duration_seconds The HTTP request latencies in seconds. 255 | > # TYPE echo_request_duration_seconds histogram 256 | > echo_request_duration_seconds_bucket{code="200",method="GET",url="/users",le="0.005"} 2 257 | > echo_request_duration_seconds_bucket{code="200",method="POST",url="/users",le="0.005"} 1 258 | > echo_request_duration_seconds_bucket{code="400",method="POST",url="/users",le="0.005"} 1 259 | ``` 260 | 261 | We see a request latency histogram for each of our `status`, `method`, `path` combinations similar to the one we created previously, but note that these seem to be using the `DefBuckets` for bucket delineations and `seconds` as the measured value. 262 | 263 | ::: tip **Review of `DefBuckets`** 264 | ```go 265 | // DefBuckets are the default Histogram buckets. The default buckets are 266 | // tailored to broadly measure the response time (in seconds) of a network 267 | // service. Most likely, however, you will be required to define buckets 268 | // customized to your use case. 269 | var DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 270 | ``` 271 | ::: 272 | 273 | All of our requests, being faster than 5 milliseconds, are counted in the smallest latency buckets and we don't have good precision where it counts here. Review the [Tuning Bucket Selection](simple_service.md#assessing-performance-and-tuning-histogram-bucket-selection) section for a discussion on why and how we want our requests to fall somewhere in the middle of our bucket range. Without the use of custom instrumentation middleware, we won't be able to track our quick response timings precisely. 274 | 275 | ### Request and Response Size Histograms 276 | 277 | ```shell 278 | > # HELP echo_request_size_bytes The HTTP request sizes in bytes. 279 | > # TYPE echo_request_size_bytes histogram 280 | > echo_request_size_bytes_bucket{code="200",method="GET",url="/users",le="1024"} 2 281 | > echo_request_size_bytes_bucket{code="200",method="POST",url="/users",le="1024"} 1 282 | > echo_request_size_bytes_bucket{code="400",method="POST",url="/users",le="1024"} 1 283 | > ... 284 | > # HELP echo_response_size_bytes The HTTP response sizes in bytes. 285 | > # TYPE echo_response_size_bytes histogram 286 | > echo_response_size_bytes_bucket{code="200",method="GET",url="/users",le="1024"} 2 287 | > echo_response_size_bytes_bucket{code="200",method="POST",url="/users",le="1024"} 1 288 | > echo_response_size_bytes_bucket{code="400",method="POST",url="/users",le="1024"} 1 289 | ``` 290 | 291 | Looking at the rest of the histograms, we're also tracking request size and response size for each of our `status`, `method`, `path` combinations. 292 | 293 | These metrics are counting size in `bytes` with a bucket range from `1024` bytes or `1KiB` (note we're talking about Kibibytes here) to `1.048576e+07` bytes or `10MiB` (also note we're talking about Mebibytes here). 294 | 295 | Since our requests and responses are much smaller than `1KiB` for our sample app, all of our requests and responses fall into the smallest size buckets. Without the use of custom instrumentation middleware, we won't be able to track our small request sizes precisely. 296 | 297 | ### Request Counters 298 | 299 | Finally let's have a look at the counters. 300 | 301 | It seems like Echo is also providing a convenient `Counter` metric type for each of our `status`, `method`, `path` combinations. While we could pull these same numbers from the `..._count` series on any of our previous histograms, there is still a level of convenience in having an explicit request counter metric when exploring metrics with a tool like PromQL or in Grafana. 302 | 303 | ```shell 304 | > # HELP echo_requests_total How many HTTP requests processed, partitioned by status code and HTTP method. 305 | > # TYPE echo_requests_total counter 306 | > echo_requests_total{code="200",host="localhost:8080",method="GET",url="/users"} 2 307 | > echo_requests_total{code="200",host="localhost:8080",method="POST",url="/users"} 1 308 | > echo_requests_total{code="400",host="localhost:8080",method="POST",url="/users"} 1 309 | ``` 310 | 311 | ## Adding Custom Metrics to Echo's Prometheus Instrumentation 312 | 313 | Echo's included Prometheus Instrumentation middleware contains support for additional custom metrics. The patterns they establish in their metrics middleware allow you to define metrics where you wire up your API, then pass the custom metrics into the request's `echo.Context` so you can decide in any of your handlers when you want to Observe a new metric value. 314 | 315 | I won't dive into this pattern in these docs here as it's a bit out of scope, but you can read Echo's documentation and code example for custom metrics [here](https://echo.labstack.com/middleware/prometheus/#serving-custom-prometheus-metrics). 316 | 317 | ## Conclusion 318 | 319 | Clearly having a standard and framework-supported package for instrumenting your API routes can take a lot of the work of building custom middleware off of your plate; it only took three lines of code to add some solid baseline metrics to our Echo API. 320 | 321 | If the unit precision and bucket spacing works for the requests and responses handled by your service, then that's awesome! Much less work for you! 322 | 323 | That being said, for our example service (and potentially for services you might be running in production), the default units of measurement and bucket spacings were insufficient to precisely measure both the response times of our routes, and the sizes of both requests and responses. 324 | 325 | Without writing custom Echo middleware to instrument our service or potentially leveraging the advanced configuration options for the `echo-contrib/prometheus` package, these metrics will leave us hanging in an incident and fail to provide additional insight into our service. 326 | 327 | This demonstrates an important lesson: _off-the-shelf instrumentation libraries may save you time upfront, but if you don't double check that the defaults are good enough for your use-case you'll end up feeling the pain when triaging incidents_. 328 | -------------------------------------------------------------------------------- /docs/observability/metrics/instrumenting_go/gin.md: -------------------------------------------------------------------------------- 1 | # Instrumenting a Gin App 2 | 3 | First let's scaffold the example service. 4 | 5 | ## Example Gin Service 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "net/http" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // User is a struct representing a user object 17 | type User struct { 18 | ID int `json:"id"` 19 | Name string `json:"name"` 20 | } 21 | 22 | var users []User 23 | 24 | func main() { 25 | router := gin.Default() 26 | 27 | router.GET("/users", listUsers) 28 | router.POST("/users", createUser) 29 | 30 | router.Run(":8080") 31 | } 32 | 33 | func listUsers(c *gin.Context) { 34 | // List all users 35 | c.JSON(http.StatusOK, users) 36 | } 37 | 38 | func createUser(c *gin.Context) { 39 | // Create a new user 40 | var user User 41 | if err := c.ShouldBindJSON(&user); err != nil { 42 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | users = append(users, user) 46 | c.JSON(http.StatusOK, user) 47 | } 48 | ``` 49 | 50 | This service is nearly identical to our trivial example but has separate handler functions for creating and listing users. We're also using the `gin` framework so our handlers take a `*gin.Context` instead of a `http.ResponseWriter` and `*http.Request`. 51 | 52 | ## Writing Gin Instrumentation Middleware 53 | 54 | In the world of Gin, we can create a custom Middleware and hook it into our request handling path using `router.Use()`. This tutorial won't go into depth on how to write custom Gin middleware but we will introduce an instrumentation middleware and explain it below: 55 | 56 | First, let's add our Prometheus libraries to the import list: 57 | 58 | ```go 59 | import ( 60 | "fmt" 61 | "net/http" 62 | "time" 63 | 64 | "github.com/gin-gonic/gin" 65 | "github.com/prometheus/client_golang/prometheus" 66 | "github.com/prometheus/client_golang/prometheus/promauto" 67 | "github.com/prometheus/client_golang/prometheus/promhttp" 68 | ) 69 | ``` 70 | 71 | Next we'll define our `reqLatency` metric just like in the previous section: 72 | 73 | ```go 74 | //... 75 | // Define the HistogramVec to keep track of Latency of Request Handling 76 | // Also declare the labels names "method", "path", and "status" 77 | var reqLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ 78 | Name: "userapi_request_latency_ms", 79 | Help: "The latency of handling requests in milliseconds", 80 | }, []string{"method", "path", "status"}) 81 | ``` 82 | 83 | Now we'll define a Middleware function that executes before our request handlers, starts a timer, invokes the handler using `c.Next()`, stops the timer and observes the `msLatency` (in `float64` milliseconds), `status`, `method`, and `path` of the request that was just handled. 84 | 85 | ```go 86 | // InstrumentationMiddleware defines our Middleware function 87 | func InstrumentationMiddleware() gin.HandlerFunc { 88 | return func(c *gin.Context) { 89 | // Everything above `c.Next()` will run before the route handler is called 90 | 91 | // Save the start time for request processing 92 | t := time.Now() 93 | 94 | // Pass the request to its handler 95 | c.Next() 96 | 97 | // Now that we've handled the request 98 | // observe the latency, method, path, and status 99 | 100 | // Stop the timer we've been using and grab the millisecond duration float64 101 | msLatency := float64(time.Since(t).Microseconds()) / 1000 102 | 103 | // Extract the Method from the request 104 | method := c.Request.Method 105 | 106 | // Grab the parameterized path from the Gin context 107 | // This preserves the template so we would get: 108 | // "/users/:name" instead of "/users/alice", "/users/bob" etc. 109 | path := c.FullPath() 110 | 111 | // Grab the status from the response writer 112 | status := c.Writer.Status() 113 | 114 | // Record the Request Latency observation 115 | reqLatency.With(prometheus.Labels{ 116 | "method": method, 117 | "path": path, 118 | "status": fmt.Sprintf("%d", status), 119 | }).Observe(msLatency) 120 | } 121 | } 122 | ``` 123 | 124 | Finally, we plug in the `InstrumentationMiddleware()` above our route handlers and wire up the `promhttp.Handler()` request handler using `gin.WrapH()` which is used to wrap vanilla `net/http` style Go handlers in `gin`-compatible semantics. 125 | 126 | ```go 127 | func main() { 128 | router := gin.Default() 129 | 130 | // Plug our middleware in before we route the request handlers 131 | router.Use(InstrumentationMiddleware()) 132 | 133 | // Wrap the promhttp handler in Gin calling semantics 134 | router.GET("/metrics", gin.WrapH(promhttp.Handler())) 135 | 136 | router.GET("/users", listUsers) 137 | router.POST("/users", createUser) 138 | 139 | router.Run(":8080") 140 | } 141 | ``` 142 | 143 | With this completed, any handler present in our Gin server declared after our `InstrumentationMiddleware()` is plugged in will be instrumented and observed in `reqLatency`. In this case, this will include the default `/metrics` handler from `promhttp` since we route it after plugging in the middleware. 144 | 145 | ## Testing the Gin Instrumentation middleware 146 | 147 | We can start up our server listening on port `8080` with: 148 | 149 | ```shell 150 | go run main.go 151 | ``` 152 | 153 | We'll run the following requests to generate some `/metrics` histograms and we can see how it compares to the histograms we saw in the prior chapter: 154 | 155 | ```shell 156 | $ curl http://localhost:8080/users 157 | > null 158 | $ curl -X POST -d'{"name":"Eric","id":1}' http://localhost:8080/users 159 | > {"id":1,"name":"Eric"} 160 | $ curl http://localhost:8080/users 161 | > [{"id":1,"name":"Eric"}] 162 | $ curl -X POST -d'{"name":' http://localhost:8080/users 163 | > {"error":"unexpected EOF"} 164 | ``` 165 | 166 | With some data in place, we can `GET` the `/metrics` endpoint to see our histograms. 167 | 168 | ```shell 169 | $ curl http://localhost:8080/metrics 170 | > # HELP userapi_request_latency_ms The latency of handling requests in milliseconds 171 | > # TYPE userapi_request_latency_ms histogram 172 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.005"} 0 173 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.01"} 0 174 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.025"} 0 175 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.05"} 1 176 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.1"} 2 177 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.25"} 2 178 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.5"} 2 179 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="1"} 2 180 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="2.5"} 2 181 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="5"} 2 182 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="10"} 2 183 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="+Inf"} 2 184 | > userapi_request_latency_ms_sum{method="GET",path="/users",status="200"} 0.123 185 | > userapi_request_latency_ms_count{method="GET",path="/users",status="200"} 2 186 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.005"} 0 187 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.01"} 0 188 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.025"} 0 189 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.05"} 0 190 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.1"} 0 191 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.25"} 1 192 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.5"} 1 193 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="1"} 1 194 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="2.5"} 1 195 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="5"} 1 196 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="10"} 1 197 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="+Inf"} 1 198 | > userapi_request_latency_ms_sum{method="POST",path="/users",status="200"} 0.132 199 | > userapi_request_latency_ms_count{method="POST",path="/users",status="200"} 1 200 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.005"} 0 201 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.01"} 0 202 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.025"} 0 203 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.05"} 0 204 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.1"} 1 205 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.25"} 1 206 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.5"} 1 207 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="1"} 1 208 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="2.5"} 1 209 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="5"} 1 210 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="10"} 1 211 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="+Inf"} 1 212 | > userapi_request_latency_ms_sum{method="POST",path="/users",status="400"} 0.054 213 | > userapi_request_latency_ms_count{method="POST",path="/users",status="400"} 1 214 | ``` 215 | 216 | We see three histograms as expected, one for the `GET /users` route and two for `POST /users` (one for status `200` and another for `400`). The `/metrics` endpoint doesn't show a histogram yet because the first time it was invoked was while processing this request and we don't `Observe()` data until after we've written a response. 217 | 218 | If we dissect our histogram results like in the simplified example, the `GET /users` endpoint averages `61.5` microseconds, the `POST /users 200` endpoint took `132` microseconds, and the `POST /users 400` endpoint took `54` microseconds. 219 | 220 | ## Conclusion 221 | 222 | This example shows we can write our own instrumentation middleware in a few lines of extra code when starting up our Gin server. We're able to tune our measurements to ensure the metric we care about falls somewhere in the middle of our histogram buckets so we can keep precise measurements, and we can easily add additional metrics to our custom middleware if we want to track additional values for each request. 223 | 224 | There is an off-the-shelf solution for Gin that requires even less configuration in the form of the [`go-gin-prometheus`](https://github.com/ericvolp12/go-gin-prometheus) library. I maintain a fork of this library that has a few quality-of-life changes to avoid [cardinality explosions](../labels.md#label-cardinality) around paths, and instrumenting your Gin service becomes as easy as: 225 | 226 | ```go 227 | import ( 228 | //... 229 | "github.com/ericvolp12/go-gin-prometheus" 230 | //... 231 | ) 232 | 233 | func main(){ 234 | r := gin.New() 235 | p := ginprometheus.NewPrometheus("gin") 236 | p.Use(r) 237 | // ... wire up your endpoints and do everything else 238 | } 239 | ``` 240 | 241 | This middleware tracks request counts, duration, and size, as well as response sizes. We'll explore the limitations of off-the-shelf instrumentation middlewares in the next section on Echo, but to summarize: sometimes the default units of measurement and bucket spacings of off-the-shelf instrumentation middleware are insufficient to precisely measure the response times of our routes and the sizes of requests and responses. 242 | 243 | The `go-gin-prometheus` middleware similarly measures response times in seconds, not milliseconds, and it uses the default Prometheus `DefBuckets` spacing meaning the shortest request we can reasonably measure will be `5ms` and the longest will be `10s`. 244 | 245 | That being said, if you have a compelling reason to write your own instrumentation middleware (like having response sizes and latencies that fall outside of the default buckets) or need to track additional metrics in your API that aren't just related to requests, you should now feel empowered to write your own instrumentation middleware for Gin applications. 246 | -------------------------------------------------------------------------------- /docs/observability/metrics/instrumenting_go/intro.md: -------------------------------------------------------------------------------- 1 | # Go Instrumentation 2 | 3 | This chapter will cover the process of adding Prometheus-style metrics instrumentation to your new or existing Go application. 4 | 5 | Some of the following examples will be particular to HTTP APIs, but I'll also include an example of instrumenting a batch-style process that exits after completion and a long-running service that may not expose a traditional HTTP API. 6 | 7 | ## The Go Prometheus Library 8 | 9 | Grafana provides a Prometheus metrics [client](https://github.com/prometheus/client_golang) for Golang that has remained remarkably stable over the past 8 years. 10 | 11 | While the OpenTelemetry project is incubating a Metrics package to to implement their vendor-agnostic [Metrics Data Model](https://opentelemetry.io/docs/reference/specification/metrics/data-model/), as of this writing, the package is in the [`Alpha`](https://github.com/open-telemetry/opentelemetry-go#project-status) state and is not recommended for general use until it graduates into the `Stable` state because it will likely have several breaking interface revisions before then. 12 | 13 | Several other metrics packages have emerged over this time that wrap existing metrics clients like [`go-kit`'s](https://github.com/go-kit/kit/tree/master/metrics) metrics package, but at the end of the day the Prometheus client has remained reliably stable and consistent for years and the formats and patterns it established have helped define what system metrics look like everywhere today. 14 | 15 | The package documentation lives [here](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus) and provides a basic starting example and a deeper dive into the full capabilities of the client library, but I'll provide in-depth examples in the following sections. -------------------------------------------------------------------------------- /docs/observability/metrics/instrumenting_go/middlewares.md: -------------------------------------------------------------------------------- 1 | # API Framework Instrumentation Middleware 2 | 3 | In this section we'll cover instrumenting RESTful HTTP APIs built on two common Golang frameworks: [Gin](https://github.com/gin-gonic/gin) and [Echo](https://github.com/labstack/echo). 4 | 5 | Middleware is a term used to describe functions that run before, during, or after the processing of requests for an API. 6 | 7 | Middleware is routed in popular web frameworks like a chain: the first middleware to be "wired up" (like with `router.Use()` in Gin) will execute first in the chain, then it can pass to the next middleware by invoking something like `c.Next()` in Gin or `next()` in Echo. Endpoint handlers are also Middleware, they're just usually at the bottom of the chain and don't call any `Next` middleware, when they complete the chain runs in reverse order back to the first wired middleware, invoking any code written after the call to `Next`. 8 | 9 | The general structure of a Middleware is like a handler function, I'll use a Gin `HandlerFunc` in this example but with other ecosystems it looks much the same: 10 | ```go 11 | func MyMiddleware() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | // Everything above `c.Next()` will run before the route handler is called 14 | // To start with, the gin.Context will only hold request info 15 | 16 | // Save the start time for request processing 17 | t := time.Now() 18 | 19 | // Pass the request to the next middleware in the chain 20 | c.Next() 21 | 22 | // Now the gin.Context will hold response info along with request info 23 | 24 | // Stop the timer we've been using and grab the processing time 25 | latency := time.Since(t) 26 | 27 | // Log the latency 28 | fmt.Printf("Request Latency: %+v\n", latency) 29 | 30 | } 31 | } 32 | ``` 33 | 34 | For consistency, we'll adapt a similar Users API from the previous section to serve as our example service. For a description of the API we're building, see [Defining an Example Service](./simple_service.md#defining-an-example-service). 35 | -------------------------------------------------------------------------------- /docs/observability/metrics/instrumenting_go/simple_service.md: -------------------------------------------------------------------------------- 1 | # Instrumenting a Simple HTTP Service 2 | 3 | Imagine you have a service in Golang that exposes some HTTP API routes and you're interested in tracking some metrics pertaining to these routes. Later we'll cover instrumenting more complex services and using instrumentation packages for common frameworks like [Gin](https://github.com/gin-gonic/gin) and [Echo](https://github.com/labstack/echo) to add some baseline metrics to existing services without having to manually instrument our handlers. 4 | 5 | ## Defining an Example Service 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "encoding/json" 12 | "net/http" 13 | ) 14 | 15 | // User is a struct representing a user object 16 | type User struct { 17 | ID int `json:"id"` 18 | Name string `json:"name"` 19 | } 20 | 21 | var users []User 22 | 23 | func main() { 24 | http.HandleFunc("/users", handleUsers) 25 | http.ListenAndServe(":8080", nil) 26 | } 27 | 28 | func handleUsers(w http.ResponseWriter, r *http.Request) { 29 | switch r.Method { 30 | case "GET": 31 | // List all users 32 | json.NewEncoder(w).Encode(users) 33 | case "POST": 34 | // Create a new user 35 | var user User 36 | json.NewDecoder(r.Body).Decode(&user) 37 | users = append(users, user) 38 | json.NewEncoder(w).Encode(user) 39 | default: 40 | http.Error( 41 | w, 42 | http.StatusText(http.StatusMethodNotAllowed), 43 | http.StatusMethodNotAllowed, 44 | ) 45 | } 46 | } 47 | 48 | ``` 49 | 50 | This code defines a struct called `User` that represents a user object. It has two fields: `ID` and `Name`. 51 | 52 | The `main()` function sets up an HTTP server that listens on port `8080` and registers a handler function for requests to the `/users` endpoint. 53 | 54 | The `handleUsers()` function is the handler for requests to the `/users` endpoint. It uses a `switch` statement to handle different HTTP methods (`GET`, `POST`, etc.) differently. 55 | 56 | For example, when a `GET` request is received, it simply encodes the list of users as JSON and writes it to the response. When a `POST` request is received, it decodes the request body as a `User` object, appends it to the list of users, and then encodes the `User` object as JSON and writes it to the response. 57 | 58 | ## Instrumenting the Example Service 59 | 60 | Metrics we may be interested in tracking include **which routes** are called in a time period, **how many times** they're called, **how long** they take to handle, and **what status code** they return. 61 | 62 | ::: info An Aside on Collectors, Gatherers, and Registries 63 | The Prometheus client library initializes one or more Metrics [Registries](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Registry) which are then periodically Collected, Gathered, and Exposed generally via an HTTP route like `/metrics` for scraping via library-managed goroutines. 64 | 65 | For our purposes, we can generally rely on the implicit Global Registry to register our metrics and use the `promauto` package to initialize our Collectors behind the scenes. If you are a power user that wants to dig deeper into building custom Metrics Registries, Collectors or Gatherers, you can take a deeper dive into the docs [here](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#hdr-Advanced_Uses_of_the_Registry). 66 | ::: 67 | 68 | 69 | We'll import three packages at the top of our file: 70 | 71 | ```go 72 | import ( 73 | //... 74 | "github.com/prometheus/client_golang/prometheus" 75 | "github.com/prometheus/client_golang/prometheus/promauto" 76 | "github.com/prometheus/client_golang/prometheus/promhttp" 77 | ) 78 | ``` 79 | 80 | The `prometheus` package is our client library, `promauto` handles registries and collectors for us, and `promhttp` will let us export our metrics to a provided HTTP Handler function so that our metrics can be scraped from `/metrics`. 81 | 82 | ### Registering Metrics and Scraping Handler 83 | 84 | Now we can initialize a `CounterVec` to keep track of calls to our API routes and use some labels on the counter to differentiate between the HTTP Method being used (`POST` vs `GET`). 85 | 86 | A `CounterVec` is a group of `Counter` metrics that may have different label values, if we just used a `Counter` we'd have to define a different metric for each distinct label value. 87 | 88 | When initializing the `CounterVec` we provide the `keys` or names of the labels in advance for registration, while the label `values` can be defined dynamically in our application when recording a metric observation. 89 | 90 | Let's initialize our `reqCounter` `CounterVec` above the `main()` function and use the `promhttp` library to expose our metrics on `/metrics`: 91 | 92 | ```go 93 | //... 94 | var users []User 95 | 96 | // Define the CounterVec to keep track of Total Number of Requests 97 | // Also declare the labels names "method" and "path" 98 | var reqCounter = promauto.NewCounterVec(prometheus.CounterOpts{ 99 | Name: "userapi_requests_handled_total", 100 | Help: "The total number of handled requests", 101 | }, []string{"method", "path"}) 102 | 103 | func main() { 104 | // Expose Prometheus Metrics on /metrics 105 | http.Handle("/metrics", promhttp.Handler()) 106 | 107 | // Register API route handlers 108 | http.HandleFunc("/users", handleUsers) 109 | 110 | // Startup the HTTP server on port 8080 111 | http.ListenAndServe(":8080", nil) 112 | } 113 | //... 114 | ``` 115 | 116 | ### Recording Observations of Custom Metrics 117 | 118 | Finally we'll want to update our `handleUsers()` function to increment the `Counter` with the proper labels when we get requests as follows: 119 | 120 | ```go 121 | //... 122 | func handleUsers(w http.ResponseWriter, r *http.Request) { 123 | switch r.Method { 124 | case "GET": 125 | // Increment the count of /users GETs 126 | reqCounter.With(prometheus.Labels{"method": "GET", "path": "/users"}).Inc() 127 | // List all users 128 | json.NewEncoder(w).Encode(users) 129 | case "POST": 130 | // Increment the count of /users POSTs 131 | reqCounter.With(prometheus.Labels{"method": "POST", "path": "/users"}).Inc() 132 | // Create a new user 133 | var user User 134 | json.NewDecoder(r.Body).Decode(&user) 135 | users = append(users, user) 136 | json.NewEncoder(w).Encode(user) 137 | default: 138 | http.Error( 139 | w, 140 | http.StatusText(http.StatusMethodNotAllowed), 141 | http.StatusMethodNotAllowed, 142 | ) 143 | } 144 | } 145 | ``` 146 | 147 | ### Testing our Instrumentation 148 | 149 | Let's test our results by running the server, hitting the endpoints a few times, then watching the `/metrics` endpoint to see how it changes: 150 | 151 | ```shell 152 | go run main.go 153 | ``` 154 | 155 | In another tab we can use `curl` to talk to the server at `http://localhost:8080` 156 | 157 | ```shell 158 | $ # GET our /users route 159 | $ curl http://localhost:8080/users 160 | > null 161 | ``` 162 | ```shell 163 | $ # Check the /metrics endpoint to see if our metric appears 164 | $ curl http://localhost:8080/metrics 165 | > ... 166 | > # HELP userapi_requests_handled_total The total number of handled requests 167 | > # TYPE userapi_requests_handled_total counter 168 | > userapi_requests_handled_total{method="GET",path="/users"} 1 169 | ``` 170 | 171 | Note that we see a single time series under the `userapi_requests_handled_total` heading with the label values specified in our `GET` handler. 172 | 173 | ```shell 174 | $ # POST a new user and then fetch it 175 | $ curl -X POST -d'{"name":"Eric","id":1}' http://localhost:8080/users 176 | > {"id":1,"name":"Eric"} 177 | $ curl http://localhost:8080/users 178 | > [{"id":1,"name":"Eric"}] 179 | ``` 180 | 181 | We've made two more requests now, a `POST` and an additional `GET`. 182 | 183 | ```shell 184 | $ # Check the /metrics endpoint again 185 | $ curl http://localhost:8080/metrics 186 | > ... 187 | > # HELP userapi_requests_handled_total The total number of handled requests 188 | > # TYPE userapi_requests_handled_total counter 189 | > userapi_requests_handled_total{method="GET",path="/users"} 2 190 | > userapi_requests_handled_total{method="POST",path="/users"} 1 191 | ``` 192 | 193 | And we can see that the `POST` handler incremented its counter for the first time so now it shows up in the `/metrics` route as well. 194 | 195 | ### Expanding our Instrumentation 196 | 197 | Let's add the additional metrics we discussed, we're still interested in understanding the response time for each endpoint as well as the status code of each request. 198 | 199 | We can add an additional label to our existing `CounterVec` to record the status code of responses as follows: 200 | 201 | ```go 202 | //... 203 | // Define the CounterVec to keep track of Total Number of Requests 204 | // Also declare the labels names "method", "path", and "status" 205 | var reqCounter = promauto.NewCounterVec(prometheus.CounterOpts{ 206 | Name: "userapi_requests_handled_total", 207 | Help: "The total number of handled requests", 208 | }, []string{"method", "path", "status"}) 209 | //... 210 | 211 | func handleUsers(w http.ResponseWriter, r *http.Request) { 212 | // Keep track of response status 213 | status := http.StatusOK 214 | switch r.Method { 215 | case "GET": 216 | // List all users 217 | err := json.NewEncoder(w).Encode(users) 218 | 219 | // Return an error if something goes wrong 220 | if err != nil { 221 | http.Error( 222 | w, 223 | http.StatusText(http.StatusInternalServerError), 224 | http.StatusInternalServerError, 225 | ) 226 | status = http.StatusInternalServerError 227 | } 228 | // Increment the count of /users GETs 229 | reqCounter.With(prometheus.Labels{ 230 | "method": "GET", 231 | "path": "/users", 232 | "status": fmt.Sprintf("%d", status), 233 | }).Inc() 234 | case "POST": 235 | // Create a new user 236 | var user User 237 | err := json.NewDecoder(r.Body).Decode(&user) 238 | // Return an error if we fail to decode the body 239 | if err != nil { 240 | http.Error( 241 | w, 242 | http.StatusText(http.StatusBadRequest), 243 | http.StatusBadRequest, 244 | ) 245 | status = http.StatusBadRequest 246 | } else { 247 | users = append(users, user) 248 | err = json.NewEncoder(w).Encode(user) 249 | 250 | // Return an error if can't encode the user for a response 251 | if err != nil { 252 | http.Error( 253 | w, 254 | http.StatusText(http.StatusInternalServerError), 255 | http.StatusInternalServerError, 256 | ) 257 | status = http.StatusInternalServerError 258 | } 259 | } 260 | // Increment the count of /users POSTs 261 | reqCounter.With(prometheus.Labels{ 262 | "method": "POST", 263 | "path": "/users", 264 | "status": fmt.Sprintf("%d", status), 265 | }).Inc() 266 | default: 267 | http.Error( 268 | w, 269 | http.StatusText(http.StatusMethodNotAllowed), 270 | http.StatusMethodNotAllowed, 271 | ) 272 | } 273 | } 274 | ``` 275 | 276 | You can see here our code is beginning to look like it needs some refactoring, this is where frameworks like [Gin](https://github.com/gin-gonic/gin) and [Echo](https://github.com/labstack/echo) can be very useful, they provide middleware interfaces that allow you to run handler hooks before and/or after the business logic of a request handler so we could instrument inside a middleware instead. 277 | 278 | Running the same series of requests as before through our application now gives us the following response on the `/metrics` endpoint: 279 | 280 | ```shell 281 | $ # Check the /metrics endpoint 282 | $ curl http://localhost:8080/metrics 283 | > ... 284 | > # HELP userapi_requests_handled_total The total number of handled requests 285 | > # TYPE userapi_requests_handled_total counter 286 | > userapi_requests_handled_total{method="GET",path="/users",status="200"} 2 287 | > userapi_requests_handled_total{method="POST",path="/users",status="200"} 1 288 | ``` 289 | 290 | We can then trigger an error by providing invalid JSON to the `POST` endpoint: 291 | 292 | ```shell 293 | $ curl -X POST -d'{"name":}' http://localhost:8080/users 294 | > Bad Request 295 | ``` 296 | 297 | And if we check the `/metrics` endpoint again we should see a new series with the status value of `400`: 298 | 299 | ```shell 300 | $ # Check the /metrics endpoint again 301 | $ curl http://localhost:8080/metrics 302 | > ... 303 | > # HELP userapi_requests_handled_total The total number of handled requests 304 | > # TYPE userapi_requests_handled_total counter 305 | > userapi_requests_handled_total{method="GET",path="/users",status="200"} 2 306 | > userapi_requests_handled_total{method="POST",path="/users",status="200"} 1 307 | > userapi_requests_handled_total{method="POST",path="/users",status="400"} 1 308 | ``` 309 | 310 | ### Using Histograms to Track Latency 311 | 312 | Fantastic! Now we have **which routes** are called in a time period, **how many times** they're called, and **what status code** they return. All we're missing is **how long** they take to handle, which will require us to use a `Histogram` instead of a `Counter`. 313 | 314 | To track response latency, we can refactor our existing metric instead of creating another one since `Histograms` also track the total number of observations in the series, they act as a `CounterVec` with a built-in label (`le`) for the upper boundary of each bucket. 315 | 316 | Let's redefine our `reqCounter` and rename it to `reqLatency`: 317 | 318 | ```go 319 | //... 320 | // Define the HistogramVec to keep track of Latency of Request Handling 321 | // Also declare the labels names "method", "path", and "status" 322 | var reqLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ 323 | Name: "userapi_request_latency_seconds", 324 | Help: "The latency of handling requests in seconds", 325 | }, []string{"method", "path", "status"}) 326 | //... 327 | ``` 328 | 329 | When defining this `HistogramVec`, we have the option to provide `Buckets` in the `HistogramOpts` which determine the thresholds for each bucket in each `Histogram`. 330 | 331 | By default, the Prometheus library will use the default bucket list `DefBuckets`: 332 | 333 | ```go 334 | // DefBuckets are the default Histogram buckets. The default buckets are 335 | // tailored to broadly measure the response time (in seconds) of a network 336 | // service. Most likely, however, you will be required to define buckets 337 | // customized to your use case. 338 | var DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 339 | ``` 340 | 341 | In our case, we can keep the default buckets as they are, but once we start measuring we might notice our responses are rather quick (since our service is just serving small amounts of data from memory) and may wish to tweak the buckets further. Defining the right buckets ahead of time can save you pain in the future when debugging observations that happen above the highest or below the lowest bucket thresholds. 342 | 343 | Remember the highest bucket being 10 seconds means we record any request that takes longer than 10 seconds as having taken between 10 and +infinity seconds: 344 | \\[ (10, +\infty]\ sec\\] 345 | 346 | The lowest threshold being 5 milliseconds means we record any request that takes fewer than 5 milliseconds to serve as having taken between 5 and 0 milliseconds: 347 | \\[ (0, 0.005]\ sec\\] 348 | 349 | 350 | Now we'll update our `handleUsers()` function to time the request duration and record the observations: 351 | 352 | ```go 353 | //... 354 | func handleUsers(w http.ResponseWriter, r *http.Request) { 355 | // Record the start time for request handling 356 | start := time.Now() 357 | status := http.StatusOK 358 | 359 | switch r.Method { 360 | case "GET": 361 | // List all users 362 | err := json.NewEncoder(w).Encode(users) 363 | 364 | // Return an error if something goes wrong 365 | if err != nil { 366 | http.Error( 367 | w, 368 | http.StatusText(http.StatusInternalServerError), 369 | http.StatusInternalServerError, 370 | ) 371 | status = http.StatusInternalServerError 372 | } 373 | // Observe the Seconds we started handling the GET 374 | reqLatency.With(prometheus.Labels{ 375 | "method": "GET", 376 | "path": "/users", 377 | "status": fmt.Sprintf("%d", status), 378 | }).Observe(time.Since(start).Seconds()) 379 | case "POST": 380 | // Create a new user 381 | var user User 382 | err := json.NewDecoder(r.Body).Decode(&user) 383 | // Return an error if we fail to decode the body 384 | if err != nil { 385 | http.Error( 386 | w, 387 | http.StatusText(http.StatusBadRequest), 388 | http.StatusBadRequest, 389 | ) 390 | status = http.StatusBadRequest 391 | } else { 392 | users = append(users, user) 393 | err = json.NewEncoder(w).Encode(user) 394 | 395 | // Return an error if can't encode the user for a response 396 | if err != nil { 397 | http.Error( 398 | w, 399 | http.StatusText(http.StatusInternalServerError), 400 | http.StatusInternalServerError, 401 | ) 402 | status = http.StatusInternalServerError 403 | } 404 | } 405 | // Observe the Seconds we started handling the POST 406 | reqLatency.With(prometheus.Labels{ 407 | "method": "POST", 408 | "path": "/users", 409 | "status": fmt.Sprintf("%d", status), 410 | }).Observe(time.Since(start).Seconds()) 411 | default: 412 | http.Error( 413 | w, 414 | http.StatusText(http.StatusMethodNotAllowed), 415 | http.StatusMethodNotAllowed, 416 | ) 417 | } 418 | } 419 | ``` 420 | 421 | Note with the `HistogramVec` object we're using the `Observe()` method instead of the `Inc()` method. `Observe()` takes a `float64`, in the case of the default buckets for HTTP latency timing, we'll generally use `Seconds` as the denomination but based on your bucket selection and time domains for the value you're observing, you can technically use any unit you want as long as you're consistent and include it in the help text (and maybe even the metric name). 422 | 423 | ### Breaking Down Histogram Bucket Representation 424 | 425 | Now we can run our requests from the Status Code example, generating some interesting metrics to view in `/metrics`: 426 | 427 | ```shell 428 | $ # Check the /metrics endpoint 429 | $ curl http://localhost:8080/metrics 430 | > ... 431 | > # HELP userapi_request_latency_seconds The latency of handling requests in seconds 432 | > # TYPE userapi_request_latency_seconds histogram 433 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.005"} 2 434 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.01"} 2 435 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.025"} 2 436 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.05"} 2 437 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.1"} 2 438 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.25"} 2 439 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="0.5"} 2 440 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="1"} 2 441 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="2.5"} 2 442 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="5"} 2 443 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="10"} 2 444 | > userapi_request_latency_seconds_bucket{method="GET",path="/users",status="200",le="+Inf"} 2 445 | > userapi_request_latency_seconds_sum{method="GET",path="/users",status="200"} 0.00011795999999999999 446 | > userapi_request_latency_seconds_count{method="GET",path="/users",status="200"} 2 447 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.005"} 1 448 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.01"} 1 449 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.025"} 1 450 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.05"} 1 451 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.1"} 1 452 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.25"} 1 453 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="0.5"} 1 454 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="1"} 1 455 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="2.5"} 1 456 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="5"} 1 457 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="10"} 1 458 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="200",le="+Inf"} 1 459 | > userapi_request_latency_seconds_sum{method="POST",path="/users",status="200"} 9.3089e-05 460 | > userapi_request_latency_seconds_count{method="POST",path="/users",status="200"} 1 461 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.005"} 1 462 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.01"} 1 463 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.025"} 1 464 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.05"} 1 465 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.1"} 1 466 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.25"} 1 467 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="0.5"} 1 468 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="1"} 1 469 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="2.5"} 1 470 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="5"} 1 471 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="10"} 1 472 | > userapi_request_latency_seconds_bucket{method="POST",path="/users",status="400",le="+Inf"} 1 473 | > userapi_request_latency_seconds_sum{method="POST",path="/users",status="400"} 7.6479e-05 474 | > userapi_request_latency_seconds_count{method="POST",path="/users",status="400"} 1 475 | ``` 476 | 477 | In the response we can see three `Histograms` represented, one for `GET /users` with a `200` status, a second for `POST /users` with a `200` status, and a third for `POST /users` with a `400` status. 478 | 479 | If we read the counts generated, we can see that each of our requests was too small for the bucket precision we chose. Prometheus records `Histogram` observations by incrementing the counter of every bucket that the observation fits in. 480 | 481 | Each bucket is defined by a `le` label that sets the condition: "every observation where the observation value is less than or equal to my `le` value and every other label value matches mine should increment my counter". Since all `le` buckets have the same values and we know we only executed four requests, each of our requests was quick enough to match the smallest latency bucket. 482 | 483 | ### Assessing Performance and Tuning Histogram Bucket Selection 484 | 485 | We can look at the `userapi_request_latency_seconds_sum` values to determine the actual latencies of our requests since these are `float64` counters that increment by the exact `float64` value observed by each histogram. 486 | 487 | ```shell 488 | > ..._seconds_sum{method="GET",path="/users",status="200"} 0.00011795999999999999 489 | > ..._seconds_count{method="GET",path="/users",status="200"} 2 490 | 491 | > ..._seconds_sum{method="POST",path="/users",status="200"} 9.3089e-05 492 | > ..._seconds_count{method="POST",path="/users",status="200"} 1 493 | 494 | > ..._seconds_sum{method="POST",path="/users",status="400"} 7.6479e-05 495 | > ..._seconds_count{method="POST",path="/users",status="400"} 1 496 | ``` 497 | 498 | When isolated, we can take the `..._sum` counter for a `Histogram` and divide it by the `..._count` counter to get an average value of all observations in the `Histogram`. 499 | 500 | Our `POST` endpoint with `200` status only has one observation with a request latency of `9.3089e-05` or `93.089` microseconds (not milliseconds). `POST` with status `400` responded in `76.479` microseconds, even quicker. And finally the average of our two `GET` requests comes down to `58.98` microseconds. 501 | 502 | Clearly this service is incredibly quick so if we want to accurately measure latencies, we'll need microsecond-level precision in our observations and will want our buckets to measure somewhere from maybe 10 microseconds at the low end to maybe 10 milliseconds at the high-end. 503 | 504 | We can update our `HistogramVec` to track `milliseconds` instead of `seconds` and then using the same default buckets we'll be tracking from `0.005` milliseconds (which is 5 microseconds) to `10` milliseconds using the same `DefBuckets`. 505 | 506 | Go tracks `Milliseconds` on a `time.Duration` as an `int64` value so to get the required precision we'll want to use the `Microseconds` value on our `time.Duration` (which is also an `int64`), cast it to a `float64`, and divide it by `1000` to make it into a `float64` of milliseconds. 507 | 508 | Make sure to cast the `Microseconds` to a `float64` before dividing by `1000` otherwise you'll end up performing integer division on the value and get rounded to the closest whole millisecond (which is zero for the requests we've seen so far). 509 | 510 | ```go 511 | //... 512 | // Define the HistogramVec to keep track of Latency of Request Handling 513 | // Also declare the labels names "method", "path", and "status" 514 | var reqLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ 515 | Name: "userapi_request_latency_ms", 516 | Help: "The latency of handling requests in milliseconds", 517 | }, []string{"method", "path", "status"}) 518 | //... 519 | // Observe the Milliseconds we started handling the GET 520 | reqLatency.With(prometheus.Labels{ 521 | "method": "GET", 522 | "path": "/users", 523 | "status": fmt.Sprintf("%d", status), 524 | }).Observe(float64(time.Since(start).Microseconds()) / 1000) 525 | //... 526 | // Observe the Milliseconds we started handling the POST 527 | reqLatency.With(prometheus.Labels{ 528 | "method": "POST", 529 | "path": "/users", 530 | "status": fmt.Sprintf("%d", status), 531 | }).Observe(float64(time.Since(start).Microseconds()) / 1000) 532 | ``` 533 | 534 | Now we can run our requests again and check the `/metrics` endpoint: 535 | 536 | ```shell 537 | $ # Check the /metrics endpoint 538 | $ curl http://localhost:8080/metrics 539 | > ... 540 | > # HELP userapi_request_latency_ms The latency of handling requests in milliseconds 541 | > # TYPE userapi_request_latency_ms histogram 542 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.005"} 0 543 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.01"} 0 544 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.025"} 0 545 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.05"} 2 546 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.1"} 2 547 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.25"} 2 548 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="0.5"} 2 549 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="1"} 2 550 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="2.5"} 2 551 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="5"} 2 552 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="10"} 2 553 | > userapi_request_latency_ms_bucket{method="GET",path="/users",status="200",le="+Inf"} 2 554 | > userapi_request_latency_ms_sum{method="GET",path="/users",status="200"} 0.082 555 | > userapi_request_latency_ms_count{method="GET",path="/users",status="200"} 2 556 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.005"} 0 557 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.01"} 0 558 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.025"} 0 559 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.05"} 0 560 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.1"} 1 561 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.25"} 1 562 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="0.5"} 1 563 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="1"} 1 564 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="2.5"} 1 565 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="5"} 1 566 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="10"} 1 567 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="200",le="+Inf"} 1 568 | > userapi_request_latency_ms_sum{method="POST",path="/users",status="200"} 0.087 569 | > userapi_request_latency_ms_count{method="POST",path="/users",status="200"} 1 570 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.005"} 0 571 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.01"} 0 572 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.025"} 0 573 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.05"} 0 574 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.1"} 1 575 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.25"} 1 576 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="0.5"} 1 577 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="1"} 1 578 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="2.5"} 1 579 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="5"} 1 580 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="10"} 1 581 | > userapi_request_latency_ms_bucket{method="POST",path="/users",status="400",le="+Inf"} 1 582 | > userapi_request_latency_ms_sum{method="POST",path="/users",status="400"} 0.085 583 | > userapi_request_latency_ms_count{method="POST",path="/users",status="400"} 1 584 | ``` 585 | 586 | Incredible! We can now spot the bucket boundary where our requests fall by looking for the `0->1` or `0->2` transition. When looking at two sequential buckets, the lower `le` label value describes the exclusive lower range of the values while the larger `le` label value describes the inclusive upper range of the bucket. 587 | 588 | Our `GET /users 200` requests seem to have both fallen in the `(0.025 - 0.05]` millisecond bucket, which would clock them somewhere between `25` and `50` microseconds. The `POST /users 200` and `POST /users 400` requests fall within the `(0.05 - 0.1]` millisecond bucket which clocks them between `50` and `100` microseconds. 589 | 590 | If we look at the `sum` and `count` values for each `Histogram` this time around we can see that the `POST` requests each took around `85` microseconds to handle, and the `GET` request took around `41` microseconds to handle. These results validate our analysis and interpretation of the Histogram buckets. 591 | 592 | In the next section we'll cover how to instrument more complex APIs that make use of the popular Go microservice routers and frameworks [Gin](https://github.com/gin-gonic/gin) and [Echo](https://github.com/labstack/echo), the two most common RESTful API frameworks for Go. 593 | -------------------------------------------------------------------------------- /docs/observability/metrics/labels.md: -------------------------------------------------------------------------------- 1 | # Labels 2 | 3 | In most metrics systems, metrics are stored as multi-dimensional time series where the primary dimension is the observed value at a given time. 4 | 5 | Labels are additional categorical dimensions for a metric that allow you to differentiate observations by some property. 6 | 7 | Example of labels for metrics are as follows: 8 | 9 | | Metric | Label | 10 | | -------------------------------------- | ----------------------------------------------------- | 11 | | CPU Utilization of a system | Kubernetes Pod Name for each instance of the system | 12 | | Latency of handling an HTTP Request | Status code of the HTTP response | 13 | | Number of requests served by a service | Path requested by the user (not including parameters) | 14 | 15 | ## Label Cardinality 16 | 17 | Each metric can have any number of Labels, but for each unique value of a Label, your metrics system will need to create and maintain a whole new time series to track the Label-specific dimension of the metric. 18 | 19 | Given the example label of `Status code of the HTTP response`, if our initial metric is a simple time series like `Number of requests served by a service`, then by adding the status code Label, we're potentially creating hundreds of new time series to track our metric, one for each HTTP Status Code that can be reported by our system. Let's say that our service only uses 20 HTTP response codes, that label gets us to 20 time series (one per _unique_ label value) since our original metric is a [Counter](#counter) that only requires one initial time series to track. 20 | 21 | When adding a second label like `Kubernetes Pod Name for each instance of the system`, we aren't adding one new time series for each Pod Name but are multiplying our existing time series by the number of unique values for this new label. In this case, if we have 10 pods each running an instance of our service, we'll end up with `1 * 20 * 10 = 200` time series for our metric. 22 | 23 | You can see how a metric with 8 labels each with 5 unique values can quickly add up, and adding just one extra label to a metric with many existing labels quickly becomes unsustainable: 24 | 25 | $$1*5^{8} = 32{,}768$$ 26 | 27 | $$1*5^{9} = 1{,}953{,}125$$ 28 | 29 | > The _uniqueness_ of a label value can be described by the term _cardinality_ where a _high cardinality_ label would have _many_ unique values and a _low cardinality_ label would have _few_ unique values. 30 | 31 | An example _high cardinality_ label could be something like `IP Address of Requester` for a web service with many unique users (cardinality equal to number of unique users). 32 | 33 | An example _low cardinality_ label could be something like `HTTP Status Group (2xx,3xx,4xx,5xx) of Response` for a web service (cardinality of 4). 34 | 35 | ## Metric Dimensionality 36 | 37 | > The cost of a metric is generally proportional to its _dimensionality_ where the _dimensionality_ of a metric is the total number of time series required to keep track of the metric and its labels. 38 | 39 | In our example above, our `Number of requests served by a service` metric has a dimensionality of `1 * 20 * 10 = 200` meaning we need 200 time series in our metric system to track the metric. If we changed the `Status code of the HTTP response` label to be a `HTTP Status Group (2xx,3xx,4xx,5xx) of Response` label, our dimensionality would reduce to `1 * 4 * 10 = 40` time series which significantly reduces the cost of tracking this metric. 40 | 41 | Higher Dimensionality metrics provide more detail and help answer more specific questions, but they do so at the cost of maintaining significantly more time series (which each have an ongoing storage and processing cost). Save High Dimensionality metrics for the things that really need a high level of detail, while picking _low cardinality_ Labels for generic metrics to keep dimensionality low. There are always tradeoffs in observability between cost and detail and Metric Dimensionality is one of the best examples of such a tradeoff. 42 | -------------------------------------------------------------------------------- /docs/observability/metrics/types.md: -------------------------------------------------------------------------------- 1 | # Metric Types 2 | 3 | This section will draw heavily from the [Prometheus Docs](https://prometheus.io/docs/concepts/metric_types/) and more can be explored there but frankly they don't go super into depth into the mechanics of metric types and do a poor job of consolidating what you _need to know_. 4 | 5 | Prometheus's metrics client libraries usually expose metrics for a service at an HTTP endpoint like `/metrics`. 6 | 7 | If you visit this endpoint, you will see a newline separated list of metrics like the one below with an optional `HELP` directive describing the metric, a `TYPE` directive describing the Metric Type, and one or more lines of Values for the metric that reflect the immediate values for the given metric. 8 | 9 | ``` 10 | # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. 11 | # TYPE process_cpu_seconds_total counter 12 | process_cpu_seconds_total 693.63 13 | 14 | # HELP process_open_fds Number of open file descriptors. 15 | # TYPE process_open_fds gauge 16 | process_open_fds 16 17 | 18 | # HELP api_response_time Latency of handling an HTTP Request 19 | # TYPE api_response_time histogram 20 | api_response_time_bucket{le="0.005"} 1.887875e+06 21 | api_response_time_bucket{le="0.01"} 1.953018e+06 22 | api_response_time_bucket{le="0.025"} 1.979542e+06 23 | api_response_time_bucket{le="0.05"} 1.985364e+06 24 | api_response_time_bucket{le="0.1"} 1.98599e+06 25 | api_response_time_bucket{le="0.25"} 1.986053e+06 26 | api_response_time_bucket{le="0.5"} 1.986076e+06 27 | api_response_time_bucket{le="1"} 1.986084e+06 28 | api_response_time_bucket{le="2.5"} 1.986087e+06 29 | api_response_time_bucket{le="5"} 1.986087e+06 30 | api_response_time_bucket{le="10"} 1.986087e+06 31 | api_response_time_bucket{le="+Inf"} 1.986087e+06 32 | api_response_time_sum 3930.0361077078646 33 | api_response_time_count 1.986087e+06 34 | 35 | # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles. 36 | # TYPE go_gc_duration_seconds summary 37 | go_gc_duration_seconds{quantile="0"} 0.000130274 38 | go_gc_duration_seconds{quantile="0.25"} 0.000147971 39 | go_gc_duration_seconds{quantile="0.5"} 0.000155235 40 | go_gc_duration_seconds{quantile="0.75"} 0.000168787 41 | go_gc_duration_seconds{quantile="1"} 0.000272923 42 | go_gc_duration_seconds_sum 0.298708187 43 | go_gc_duration_seconds_count 1815 44 | ``` 45 | 46 | The Prometheus model uses Scraping by default, where some Agent will poll the `/metrics` endpoint every `n` seconds (usually 60 seconds) and record discrete values for every time series described by the endpoint. Because this polling is not continuous, when a metric is updated in your service, it may not create a discrete datapoint until the next time Scraper comes by. This is an optimization that reduces the amount of work needed to be done by both your service and the Scraper, but if you need to create datapoints synchronously (like for a batch job that exits after it finishes whether or not a Scraper has a chance to poll its `/metrics` endpoint), Prometheus has a [Push Gateway](https://prometheus.io/docs/practices/pushing/) pattern. We will dive deeper into [Pushing Metrics](#pushing-metrics) later in this chapter. -------------------------------------------------------------------------------- /docs/observability/metrics/types/counters.md: -------------------------------------------------------------------------------- 1 | # Counters 2 | 3 | The fundamental metric type (at least in Prometheus's metrics model) is a **Counter**. A Counter is a monotonically increasing (only counts up), floating point value that starts at 0 when initialized. 4 | 5 | > It's important to note that when an application starts up, all of its metrics are initialized to zero and instrumentation libraries cumulatively build on this zero value through the lifetime of the application. You may have a container with a service in it that's been running for two months where a Counter has incremented itself to `1,525,783` but as soon as the container restarts that Counter will read as `0`. 6 | 7 | Counters are useful metrics for tracking a number of times something has happened since the start of the service. Some examples of things you might track with a counter include: 8 | - Number of requests handled 9 | - Number of calls to an external service 10 | - Number of responses with a particular status code 11 | - Number of errors in processing requests 12 | - Number of jobs executed 13 | 14 | In our example payload, the Counter metric is represented as follows: 15 | 16 | ``` 17 | # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. 18 | # TYPE process_cpu_seconds_total counter 19 | process_cpu_seconds_total 693.63 20 | ``` 21 | 22 | This metric tracks the CPU time consumed by our service. It starts at 0 when the service starts and slowly climbs over time. This number will increase forever if our service never restarts. Prometheus's Counter implementation handles that by representing numbers in [E Notation](https://en.wikipedia.org/wiki/Scientific_notation#E_notation) once they get large enough. For example, `1.986087e+06` is equivalent to \\(1.986087 * 10^{6} = 1{,}986{,}087\\). Clearly, as we represent larger and larger numbers in our metrics, we lose precision, but generally the larger the numbers we are tracking, the larger the changes are and the less precision we need to identify patterns in the system. 23 | 24 | Because counters are monotonically increasing values, even though scrape intervals may be minutes apart, we can interpolate the values in between to decent accuracy. In the image below, we know the values between Scrape 1 and Scrape 2 must be somewhere between the dotted lines, so we can estimate them as a straight line if we need to guess at the value in between scrapes. 25 | 26 | ![Counter Interpolation Graph](images/counter_interpolation.png) 27 | 28 | ## Querying Counter Metrics 29 | 30 | To explore Counter metrics, let's build a Prometheus query in [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/), Prometheus's Query Language. 31 | 32 | To start, we can query our Prometheus instance for our counter metric by name: `process_cpu_seconds_total`. 33 | 34 | ![A screenshot of the Prometheus Web UI searching for "process_cpu_seconds_total"](images/counter_prom_1.png) 35 | 36 | We can see that this given Prometheus instance has eight series being returned, with each series in the form: 37 | ``` 38 | process_cpu_seconds_total{instance="", job=""} 39 | ``` 40 | 41 | Prometheus represents metrics in PromQL using the following format: 42 | 43 | ``` 44 | {="", ="", ..., =""} 45 | ``` 46 | 47 | In our example, we see two different labels, `instance` and `job`. The `instance` label has eight distinct values, as does the `job` label. Prometheus generates a new time series for each unique set of label values it encounters, so while the theoretical dimensionality of this metric may be `1 * 8 * 8 = 64`, in practice there are only eight series being maintained. 48 | 49 | Let's select one of these series to explore further. 50 | 51 | In PromQL we can narrow the time series returned by our query by specifying filters for labels. The example below restricts the above results to only include those series where the `instance` label has a value that starts with `localhost`. To do this we use the `=~` operator on the `instance` label to filter by a regex that matches anything starting with `localhost`. 52 | 53 | ``` 54 | process_cpu_seconds_total{instance=~"localhost.*"} 55 | ``` 56 | 57 | ![A screenshot of the Prometheus Web UI searching for "process_cpu_seconds_total{instance=~"localhost.*"}"](images/counter_prom_2.png) 58 | 59 | We can further filter by `job` to end up with a single series trivially using the `=` operator. Let's filter for the `prometheus` job using the following query: 60 | 61 | ``` 62 | process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"} 63 | ``` 64 | 65 | _Note that we could technically have just used the `job` query here since there is only one series with a `job` label with value `prometheus`._ 66 | 67 | ![A screenshot of the Prometheus Web UI searching for "process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"}"](images/counter_prom_3.png) 68 | 69 | From here, we can click on the `Graph` tab to view our metric in Prometheus's built-in graphing tool, though we'll go over building more interesting graphs in Grafana later. 70 | 71 | ![A screenshot of the Prometheus Web UI graphing "process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"}" with a 1 hour window](images/counter_prom_4.png) 72 | 73 | As this is a Counter metric, we expect to see a monotonically increasing graph, which is quite clear, but why does the line look perfectly smooth if we didn't do anything to interpolate values? If we set the time range to something smaller than an hour, such as five minutes, the graph starts to look a bit different. 74 | 75 | ![A screenshot of the Prometheus Web UI graphing "process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"}" with a 5 minute window](images/counter_prom_5.png) 76 | 77 | Now we can see the bumpiness we would expect of many discrete observations spaced an equal amount of time apart. 78 | 79 | An even more intense zoom reveals the gap between observations to be five seconds for our given metric. Each stair step has a length of four seconds and then the line up to the next stair has a length of one second meaning our observations are separated by a total of five seconds. 80 | 81 | ![A screenshot of the Prometheus Web UI graphing "process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"}" with a 10 second window](images/counter_prom_6.png) 82 | 83 | ## Range Vectors and the Rate Query 84 | 85 | While this graph is neat, it doesn't exactly make much sense as a raw series. Total CPU Seconds used by a service is interesting but it would be much more useful to see it as a rate of CPU usage in a format like CPU Seconds per Second. Thankfully, PromQL can help us derive a rate from this metric as follows: 86 | 87 | ``` 88 | rate(process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"}[1m]) 89 | ``` 90 | 91 | In this query we're making use of the [`rate()`](https://prometheus.io/docs/prometheus/latest/querying/functions/#rate) Query Function in PromQL which calculates the "per-second average rate of increase in the time series in the range vector". This is effectively the slope of the line of the raw observed value with a bit of added nuance. To break that down a bit, the "range vector" in our query is `[1m]`, meaning for each observation, we are grabbing the value of the metric at that time, then the values of previous observations for that metric from one minute prior to the selected observation. Once we have that list of values, we calculate the rate of increase between each successive observation, then average it out over the one minute period. For a monotonically increasing Counter value, we can simplify the equation by grabbing the first and last points in our range vector, grabbing the difference in values and dividing by the time between the two observations. Let's show it the hard way and then we can simplify. 92 | 93 | Consider the following observations for a given counter metric, `requests`, in the form `[time, counter_value]`: 94 | 95 | ``` 96 | [0, 0], [10, 4], [20, 23], [30, 31], [40, 45], [50, 63], [60, 74], [70, 102] 97 | ``` 98 | 99 | If we wanted to take the `rate(requests{}[30s])` at the point `[40, 45]` we would grab 30 seconds worth of observations, so all those going back to `[10, 4]`. Then we calculate the increase between each successive observation: 100 | - `[10, 4]` to `[20, 23]` -> `23 - 4 = 19` 101 | - `[20, 23]` to `[30, 31]` -> `31 - 23 = 8` 102 | - `[30, 31]` to `[40, 45]` -> `45 - 31 = 14` 103 | 104 | Since our observations are evenly spaced, we can average the per-second rate of increase as: 105 | \\[ \frac{19}{10} + \frac{8}{10} + \frac{14}{10} = 1.\overline{33}\\] 106 | 107 | As we're using a Counter, we can simplify the process by grabbing the first and last observations in the range and calculating the difference between them and divide by the number of seconds between the observations: 108 | \\[ \frac{45-4}{30} = 1.\overline{33}\\] 109 | 110 | That only gives us the `rate(requests{}[30s])` at the `time=40` point, but that process is repeated for every observation visible at the resolution we're requesting. 111 | 112 | The result of this operation on our `process_cpu_seconds_total` metric is graphed below: 113 | 114 | ![A screenshot of the Prometheus Web UI graphing "rate(process_cpu_seconds_total{instance=~"localhost.*", job="prometheus"}[1m])" with a 1 hour window](images/counter_prom_7.png) 115 | 116 | From this graph we can see that in the past hour our service peaks its usage at a little over `0.04` CPU Seconds per Second or around `4%` of a single CPU core. 117 | -------------------------------------------------------------------------------- /docs/observability/metrics/types/gauges.md: -------------------------------------------------------------------------------- 1 | # Gauges 2 | 3 | The second metric type we need to know about is a Gauge. 4 | 5 | Gauges are very similar to Counters in that they are a floating point value that starts initialized to zero, however a Gauge can go both up and down and has the potential to be either a positive or negative number. 6 | 7 | Gauges are useful metrics for tracking a specific internal state value that fluctuates up and down throughout service lifetime: 8 | - Temperature of a sensor 9 | - Bytes of memory currently allocated 10 | - Number of pending requests in the queue 11 | - Number of active routines 12 | - Number of active TCP connections 13 | - Number of open File Descriptors 14 | 15 | In our example payload, the Gauge metric is represented as follows: 16 | 17 | ``` 18 | # HELP process_open_fds Number of open file descriptors. 19 | # TYPE process_open_fds gauge 20 | process_open_fds 16 21 | ``` 22 | 23 | Gauges are useful to interrogate the immediate internal state of a service at a given point in time. 24 | 25 | Unlike with Counters, calculating the `rate()` of a Gauge doesn't really make sense since Gauge values can go up and down between observations. 26 | 27 | We are unable to interpolate between two observations of a Gauge since we don't know the boundaries of possible values for the Gauge between the observations. 28 | 29 | ![Graph of a Gauge with its true value wandering above and below both observed values in between them](images/gauge_interpolation.png) 30 | 31 | In the absence of a `rate()` style query, how can we contextualize a Gauge metric? 32 | 33 | Prometheus provides us with [additional Range Vector functions](https://prometheus.io/docs/prometheus/latest/querying/functions/#aggregation_over_time) to help interpret the values of Gauges by aggregating observations over time. 34 | 35 | In the previous chapter on Counters, I introduce a concept of a [Range Vector](counters.md#range-vectors-and-the-rate-query) and walk through how the `rate()` query operates on such vectors. We'll be using Range Vectors below so feel free to go back and review if you need a refresher. 36 | 37 | Below is a list of a few of the functions we can use to better understand what our Gauge is telling us about our service: 38 | - `avg_over_time()` 39 | - Take all values in the range vector, add them up, and divide the sum by the number of observations in the vector. 40 | - `min_over_time()` 41 | - Take the lowest value in the range vector (remember, Gauge values can be both positive or negative). 42 | - `max_over_time()` 43 | - Take the highest value in the range vector. 44 | - `sum_over_time()` 45 | - Take all values in the range vector and add them up. 46 | - `quantile_over_time()` 47 | - Keep reading for a breakdown. 48 | 49 | There are a few others available to us but for the purposes of this chapter we'll explore the above five functions in a practical scenario. 50 | 51 | ## The Quantile Function 52 | 53 | The [Quantile Function](https://en.wikipedia.org/wiki/Quantile_function) is a commonly used function in probability theory and statistics but I am neither a statistician or probabilist. 54 | 55 | Thankfully we don't need probability theory to understand what `quantile_over_time()` does and how it works. 56 | 57 | Given a Range Vector (list of successive observations of a Gauge) `vec` and a target Quantile of `0.90`, `quantile_over_time(0.90, vec)` returns the value at the _90th percentile_ of all observations in `vec`. 58 | 59 | Recall that the _50th percentile_ of a series of numbers, also called the _median_, is the member of the series at which half of the remaining numbers in the series fall below and the other half fall above. 60 | 61 | For a sample series: 62 | 63 | ``` 64 | [25, 45, 34, 88, 76, 53, 91, 21, 53, 12, 6, 37, 97, 50] 65 | ``` 66 | 67 | We find the _median_ by sorting the series least-to-greatest and picking the value in the middle, or in the case of a series of even length, we average the two middle-most numbers. 68 | 69 | ``` 70 | [6, 12, 21, 25, 34, 37, (45, 50), 53, 53, 76, 88, 91, 97] 71 | ``` 72 | 73 | \\[ \frac{45 + 50}{2} = 47.5\\] 74 | 75 | So in this example our _median_ is `47.5` which is the same as the _50th percentile_ of the series. 76 | 77 | Okay, so what does that make the _90th percentile_? Well the _90th percentile_ of a series is the value for which 90% of the numbers in the series fall below and 10% of the numbers in the series fall above. 78 | 79 | So given our example series, how do we find the _90th percentile_? 80 | 81 | Well if we have an ordered series of numbers, we'd want to grab the value at position `0.9 * n` where `n` is the number of values in the series, since that would split the series into two chunks, one with 90% of the values and one with 10% of the values. In our example series, we have 14 values, so we'd take the value at position `0.9 * 14 = 12.6`. 82 | 83 | Since our series has 14 numbers in it, it is impossible to find a value at which exactly 90% of the values fall below it and 10% fall above it (14 is not divisible by 10), so we can estimate the _90th percentile_ for our series by taking the weighted average of the values on either side of the split point. 84 | 85 | ``` 86 | [6, 12, 21, 25, 34, 37, 45, 50, 53, 53, 76, (88, 91), 97] 87 | ``` 88 | 89 | \\[ (88 * (1-0.6)) + (91 * 0.6) = 89.8\\] 90 | 91 | 92 | Now that we've cleared that up, it should be easy to get the _75th percentile_ of our Range Vector (or any other percentile we need), just use `quantile_over_time(0.75, vec)`. 93 | 94 | If we look at the implementation of the `quantile_over_time()` function in the Prometheus [source](https://github.com/prometheus/prometheus/blob/main/promql/quantile.go#LL357-L386C2) we see a fairly straightforward Go function that does the same process we just walked through: 95 | 96 | ```go 97 | // quantile calculates the given quantile of a vector of samples. 98 | // ... 99 | func quantile(q float64, values vectorByValueHeap) float64 { 100 | //... 101 | sort.Sort(values) 102 | 103 | n := float64(len(values)) 104 | // When the quantile lies between two samples, 105 | // we use a weighted average of the two samples. 106 | // Multiplying by (n-1) because our vector starts at index 0 in code 107 | rank := q * (n - 1) 108 | 109 | lowerIndex := math.Max(0, math.Floor(rank)) 110 | upperIndex := math.Min(n-1, lowerIndex+1) 111 | 112 | weight := rank - math.Floor(rank) 113 | return values[int(lowerIndex)].V*(1-weight) + values[int(upperIndex)].V*weight 114 | } 115 | ``` 116 | 117 | ## Querying Gauge Metrics 118 | 119 | Now let's put these functions into practice and learn a bit about our example metric, `process_open_fds`. 120 | 121 | TODO: write about querying Gauge metrics 122 | -------------------------------------------------------------------------------- /docs/observability/metrics/types/histograms.md: -------------------------------------------------------------------------------- 1 | # Histograms 2 | 3 | TODO: write about histograms 4 | -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_interpolation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_interpolation.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_1.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_2.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_3.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_4.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_5.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_6.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/counter_prom_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/counter_prom_7.png -------------------------------------------------------------------------------- /docs/observability/metrics/types/images/gauge_interpolation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericvolp12/practical-observability/4221cf862935b91a3311c9d9126135b499ac4376/docs/observability/metrics/types/images/gauge_interpolation.png -------------------------------------------------------------------------------- /docs/observability/observability.md: -------------------------------------------------------------------------------- 1 | # Observability 2 | 3 | > In control theory, observability (o11y) is defined as a measure of how well internal states of a system can be inferred from knowledge of its external outputs.[^1] 4 | > 5 | > For software systems, observability is a measure of how well you can understand and explain any state your system can get into.[^1] 6 | 7 | Observability has three traditionally agreed-upon pillars and a fourth more emergent pillar: Metrics, Logging, Tracing, and Profiling. 8 | 9 | This book aims to provide a primer on each of these pillars of o11y through the lens of a common open source observability tooling vendor, [Grafana](https://grafana.com/). 10 | 11 | Through the sections of this chapter, you will be able to learn the fundamentals of Metrics, Logging, Tracing, and Profiling, see code samples in two popular coding languages (Python and Golang), and develop the skills required to create high signal, insightful dashboards to increase visibility and reduce Mean Time to Resolution for production incidents. 12 | 13 | [^1]: Lots of content for these docs is inspired and sometimes directly drawn from Charity Majors, Liz Fong-Jones, and George Miranda's book, ["Observability Engineering"](https://info.honeycomb.io/observability-engineering-oreilly-book-2022) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "practical-observability", 3 | "version": "1.0.0", 4 | "description": "A book about Observability", 5 | "devDependencies": { 6 | "doctoc": "^2.2.0", 7 | "vitepress": "^1.0.0-alpha.47", 8 | "vue": "^3.2.47" 9 | }, 10 | "scripts": { 11 | "docs:dev": "vitepress dev docs", 12 | "docs:build": "vitepress build docs", 13 | "docs:preview": "vitepress preview docs" 14 | }, 15 | "author": "ericvolp12", 16 | "main": "index.js", 17 | "repository": "git@github.com:ericvolp12/practical-observability.git", 18 | "license": "MIT", 19 | "dependencies": { 20 | "markdown-it-footnote": "^3.0.3", 21 | "markdown-it-mathjax3": "^4.3.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@algolia/autocomplete-core@1.7.4": 6 | version "1.7.4" 7 | resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz#85ff36b2673654a393c8c505345eaedd6eaa4f70" 8 | integrity sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg== 9 | dependencies: 10 | "@algolia/autocomplete-shared" "1.7.4" 11 | 12 | "@algolia/autocomplete-preset-algolia@1.7.4": 13 | version "1.7.4" 14 | resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz#610ee1d887962f230b987cba2fd6556478000bc3" 15 | integrity sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ== 16 | dependencies: 17 | "@algolia/autocomplete-shared" "1.7.4" 18 | 19 | "@algolia/autocomplete-shared@1.7.4": 20 | version "1.7.4" 21 | resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz#78aea1140a50c4d193e1f06a13b7f12c5e2cbeea" 22 | integrity sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg== 23 | 24 | "@algolia/cache-browser-local-storage@4.14.3": 25 | version "4.14.3" 26 | resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz#b9e0da012b2f124f785134a4d468ee0841b2399d" 27 | integrity sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw== 28 | dependencies: 29 | "@algolia/cache-common" "4.14.3" 30 | 31 | "@algolia/cache-common@4.14.3": 32 | version "4.14.3" 33 | resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.14.3.tgz#a78e9faee3dfec018eab7b0996e918e06b476ac7" 34 | integrity sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q== 35 | 36 | "@algolia/cache-in-memory@4.14.3": 37 | version "4.14.3" 38 | resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz#96cefb942aeb80e51e6a7e29f25f4f7f3439b736" 39 | integrity sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg== 40 | dependencies: 41 | "@algolia/cache-common" "4.14.3" 42 | 43 | "@algolia/client-account@4.14.3": 44 | version "4.14.3" 45 | resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.14.3.tgz#6d7d032a65c600339ce066505c77013d9a9e4966" 46 | integrity sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ== 47 | dependencies: 48 | "@algolia/client-common" "4.14.3" 49 | "@algolia/client-search" "4.14.3" 50 | "@algolia/transporter" "4.14.3" 51 | 52 | "@algolia/client-analytics@4.14.3": 53 | version "4.14.3" 54 | resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.14.3.tgz#ca409d00a8fff98fdcc215dc96731039900055dc" 55 | integrity sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw== 56 | dependencies: 57 | "@algolia/client-common" "4.14.3" 58 | "@algolia/client-search" "4.14.3" 59 | "@algolia/requester-common" "4.14.3" 60 | "@algolia/transporter" "4.14.3" 61 | 62 | "@algolia/client-common@4.14.3": 63 | version "4.14.3" 64 | resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.14.3.tgz#c44e48652b2121a20d7a40cfd68d095ebb4191a8" 65 | integrity sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw== 66 | dependencies: 67 | "@algolia/requester-common" "4.14.3" 68 | "@algolia/transporter" "4.14.3" 69 | 70 | "@algolia/client-personalization@4.14.3": 71 | version "4.14.3" 72 | resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.14.3.tgz#8f71325035aa2a5fa7d1d567575235cf1d6c654f" 73 | integrity sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg== 74 | dependencies: 75 | "@algolia/client-common" "4.14.3" 76 | "@algolia/requester-common" "4.14.3" 77 | "@algolia/transporter" "4.14.3" 78 | 79 | "@algolia/client-search@4.14.3": 80 | version "4.14.3" 81 | resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.14.3.tgz#cf1e77549f5c3e73408ffe6441ede985fde69da0" 82 | integrity sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A== 83 | dependencies: 84 | "@algolia/client-common" "4.14.3" 85 | "@algolia/requester-common" "4.14.3" 86 | "@algolia/transporter" "4.14.3" 87 | 88 | "@algolia/logger-common@4.14.3": 89 | version "4.14.3" 90 | resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.14.3.tgz#87d4725e7f56ea5a39b605771b7149fff62032a7" 91 | integrity sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw== 92 | 93 | "@algolia/logger-console@4.14.3": 94 | version "4.14.3" 95 | resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.14.3.tgz#1f19f8f0a5ef11f01d1f9545290eb6a89b71fb8a" 96 | integrity sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw== 97 | dependencies: 98 | "@algolia/logger-common" "4.14.3" 99 | 100 | "@algolia/requester-browser-xhr@4.14.3": 101 | version "4.14.3" 102 | resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz#bcf55cba20f58fd9bc95ee55793b5219f3ce8888" 103 | integrity sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q== 104 | dependencies: 105 | "@algolia/requester-common" "4.14.3" 106 | 107 | "@algolia/requester-common@4.14.3": 108 | version "4.14.3" 109 | resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.14.3.tgz#2d02fbe01afb7ae5651ae8dfe62d6c089f103714" 110 | integrity sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw== 111 | 112 | "@algolia/requester-node-http@4.14.3": 113 | version "4.14.3" 114 | resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz#72389e1c2e5d964702451e75e368eefe85a09d8f" 115 | integrity sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA== 116 | dependencies: 117 | "@algolia/requester-common" "4.14.3" 118 | 119 | "@algolia/transporter@4.14.3": 120 | version "4.14.3" 121 | resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.14.3.tgz#5593036bd9cf2adfd077fdc3e81d2e6118660a7a" 122 | integrity sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w== 123 | dependencies: 124 | "@algolia/cache-common" "4.14.3" 125 | "@algolia/logger-common" "4.14.3" 126 | "@algolia/requester-common" "4.14.3" 127 | 128 | "@babel/parser@^7.16.4": 129 | version "7.21.2" 130 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" 131 | integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== 132 | 133 | "@docsearch/css@3.3.3", "@docsearch/css@^3.3.3": 134 | version "3.3.3" 135 | resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.3.3.tgz#f9346c9e24602218341f51b8ba91eb9109add434" 136 | integrity sha512-6SCwI7P8ao+se1TUsdZ7B4XzL+gqeQZnBc+2EONZlcVa0dVrk0NjETxozFKgMv0eEGH8QzP1fkN+A1rH61l4eg== 137 | 138 | "@docsearch/js@^3.3.3": 139 | version "3.3.3" 140 | resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.3.3.tgz#70725a7a8fe92d221fcf0593263b936389d3728f" 141 | integrity sha512-2xAv2GFuHzzmG0SSZgf8wHX0qZX8n9Y1ZirKUk5Wrdc+vH9CL837x2hZIUdwcPZI9caBA+/CzxsS68O4waYjUQ== 142 | dependencies: 143 | "@docsearch/react" "3.3.3" 144 | preact "^10.0.0" 145 | 146 | "@docsearch/react@3.3.3": 147 | version "3.3.3" 148 | resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.3.3.tgz#907b6936a565f880b4c0892624b4f7a9f132d298" 149 | integrity sha512-pLa0cxnl+G0FuIDuYlW+EBK6Rw2jwLw9B1RHIeS4N4s2VhsfJ/wzeCi3CWcs5yVfxLd5ZK50t//TMA5e79YT7Q== 150 | dependencies: 151 | "@algolia/autocomplete-core" "1.7.4" 152 | "@algolia/autocomplete-preset-algolia" "1.7.4" 153 | "@docsearch/css" "3.3.3" 154 | algoliasearch "^4.0.0" 155 | 156 | "@esbuild/android-arm64@0.16.17": 157 | version "0.16.17" 158 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" 159 | integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== 160 | 161 | "@esbuild/android-arm@0.16.17": 162 | version "0.16.17" 163 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" 164 | integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== 165 | 166 | "@esbuild/android-x64@0.16.17": 167 | version "0.16.17" 168 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" 169 | integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== 170 | 171 | "@esbuild/darwin-arm64@0.16.17": 172 | version "0.16.17" 173 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" 174 | integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== 175 | 176 | "@esbuild/darwin-x64@0.16.17": 177 | version "0.16.17" 178 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" 179 | integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== 180 | 181 | "@esbuild/freebsd-arm64@0.16.17": 182 | version "0.16.17" 183 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" 184 | integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== 185 | 186 | "@esbuild/freebsd-x64@0.16.17": 187 | version "0.16.17" 188 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" 189 | integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== 190 | 191 | "@esbuild/linux-arm64@0.16.17": 192 | version "0.16.17" 193 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" 194 | integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== 195 | 196 | "@esbuild/linux-arm@0.16.17": 197 | version "0.16.17" 198 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" 199 | integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== 200 | 201 | "@esbuild/linux-ia32@0.16.17": 202 | version "0.16.17" 203 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" 204 | integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== 205 | 206 | "@esbuild/linux-loong64@0.16.17": 207 | version "0.16.17" 208 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" 209 | integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== 210 | 211 | "@esbuild/linux-mips64el@0.16.17": 212 | version "0.16.17" 213 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" 214 | integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== 215 | 216 | "@esbuild/linux-ppc64@0.16.17": 217 | version "0.16.17" 218 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" 219 | integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== 220 | 221 | "@esbuild/linux-riscv64@0.16.17": 222 | version "0.16.17" 223 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" 224 | integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== 225 | 226 | "@esbuild/linux-s390x@0.16.17": 227 | version "0.16.17" 228 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" 229 | integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== 230 | 231 | "@esbuild/linux-x64@0.16.17": 232 | version "0.16.17" 233 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" 234 | integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== 235 | 236 | "@esbuild/netbsd-x64@0.16.17": 237 | version "0.16.17" 238 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" 239 | integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== 240 | 241 | "@esbuild/openbsd-x64@0.16.17": 242 | version "0.16.17" 243 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" 244 | integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== 245 | 246 | "@esbuild/sunos-x64@0.16.17": 247 | version "0.16.17" 248 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" 249 | integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== 250 | 251 | "@esbuild/win32-arm64@0.16.17": 252 | version "0.16.17" 253 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" 254 | integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== 255 | 256 | "@esbuild/win32-ia32@0.16.17": 257 | version "0.16.17" 258 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" 259 | integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== 260 | 261 | "@esbuild/win32-x64@0.16.17": 262 | version "0.16.17" 263 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" 264 | integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== 265 | 266 | "@textlint/ast-node-types@^12.6.1": 267 | version "12.6.1" 268 | resolved "https://registry.yarnpkg.com/@textlint/ast-node-types/-/ast-node-types-12.6.1.tgz#35ecefe74e701d7f632c083d4fda89cab1b89012" 269 | integrity sha512-uzlJ+ZsCAyJm+lBi7j0UeBbj+Oy6w/VWoGJ3iHRHE5eZ8Z4iK66mq+PG/spupmbllLtz77OJbY89BYqgFyjXmA== 270 | 271 | "@textlint/markdown-to-ast@^12.1.1": 272 | version "12.6.1" 273 | resolved "https://registry.yarnpkg.com/@textlint/markdown-to-ast/-/markdown-to-ast-12.6.1.tgz#fcccb5733b3e76cd0db78a323763ab101f2d803b" 274 | integrity sha512-T0HO+VrU9VbLRiEx/kH4+gwGMHNMIGkp0Pok+p0I33saOOLyhfGvwOKQgvt2qkxzQEV2L5MtGB8EnW4r5d3CqQ== 275 | dependencies: 276 | "@textlint/ast-node-types" "^12.6.1" 277 | debug "^4.3.4" 278 | mdast-util-gfm-autolink-literal "^0.1.3" 279 | remark-footnotes "^3.0.0" 280 | remark-frontmatter "^3.0.0" 281 | remark-gfm "^1.0.0" 282 | remark-parse "^9.0.0" 283 | traverse "^0.6.7" 284 | unified "^9.2.2" 285 | 286 | "@types/mdast@^3.0.0": 287 | version "3.0.10" 288 | resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" 289 | integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA== 290 | dependencies: 291 | "@types/unist" "*" 292 | 293 | "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": 294 | version "2.0.6" 295 | resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" 296 | integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== 297 | 298 | "@types/web-bluetooth@^0.0.16": 299 | version "0.0.16" 300 | resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8" 301 | integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ== 302 | 303 | "@vitejs/plugin-vue@^4.0.0": 304 | version "4.0.0" 305 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz#93815beffd23db46288c787352a8ea31a0c03e5e" 306 | integrity sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA== 307 | 308 | "@vue/compiler-core@3.2.47": 309 | version "3.2.47" 310 | resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.47.tgz#3e07c684d74897ac9aa5922c520741f3029267f8" 311 | integrity sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig== 312 | dependencies: 313 | "@babel/parser" "^7.16.4" 314 | "@vue/shared" "3.2.47" 315 | estree-walker "^2.0.2" 316 | source-map "^0.6.1" 317 | 318 | "@vue/compiler-dom@3.2.47": 319 | version "3.2.47" 320 | resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305" 321 | integrity sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ== 322 | dependencies: 323 | "@vue/compiler-core" "3.2.47" 324 | "@vue/shared" "3.2.47" 325 | 326 | "@vue/compiler-sfc@3.2.47": 327 | version "3.2.47" 328 | resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz#1bdc36f6cdc1643f72e2c397eb1a398f5004ad3d" 329 | integrity sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ== 330 | dependencies: 331 | "@babel/parser" "^7.16.4" 332 | "@vue/compiler-core" "3.2.47" 333 | "@vue/compiler-dom" "3.2.47" 334 | "@vue/compiler-ssr" "3.2.47" 335 | "@vue/reactivity-transform" "3.2.47" 336 | "@vue/shared" "3.2.47" 337 | estree-walker "^2.0.2" 338 | magic-string "^0.25.7" 339 | postcss "^8.1.10" 340 | source-map "^0.6.1" 341 | 342 | "@vue/compiler-ssr@3.2.47": 343 | version "3.2.47" 344 | resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz#35872c01a273aac4d6070ab9d8da918ab13057ee" 345 | integrity sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw== 346 | dependencies: 347 | "@vue/compiler-dom" "3.2.47" 348 | "@vue/shared" "3.2.47" 349 | 350 | "@vue/devtools-api@^6.5.0": 351 | version "6.5.0" 352 | resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" 353 | integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== 354 | 355 | "@vue/reactivity-transform@3.2.47": 356 | version "3.2.47" 357 | resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e" 358 | integrity sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA== 359 | dependencies: 360 | "@babel/parser" "^7.16.4" 361 | "@vue/compiler-core" "3.2.47" 362 | "@vue/shared" "3.2.47" 363 | estree-walker "^2.0.2" 364 | magic-string "^0.25.7" 365 | 366 | "@vue/reactivity@3.2.47": 367 | version "3.2.47" 368 | resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.47.tgz#1d6399074eadfc3ed35c727e2fd707d6881140b6" 369 | integrity sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ== 370 | dependencies: 371 | "@vue/shared" "3.2.47" 372 | 373 | "@vue/runtime-core@3.2.47": 374 | version "3.2.47" 375 | resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.47.tgz#406ebade3d5551c00fc6409bbc1eeb10f32e121d" 376 | integrity sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA== 377 | dependencies: 378 | "@vue/reactivity" "3.2.47" 379 | "@vue/shared" "3.2.47" 380 | 381 | "@vue/runtime-dom@3.2.47": 382 | version "3.2.47" 383 | resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz#93e760eeaeab84dedfb7c3eaf3ed58d776299382" 384 | integrity sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA== 385 | dependencies: 386 | "@vue/runtime-core" "3.2.47" 387 | "@vue/shared" "3.2.47" 388 | csstype "^2.6.8" 389 | 390 | "@vue/server-renderer@3.2.47": 391 | version "3.2.47" 392 | resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.47.tgz#8aa1d1871fc4eb5a7851aa7f741f8f700e6de3c0" 393 | integrity sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA== 394 | dependencies: 395 | "@vue/compiler-ssr" "3.2.47" 396 | "@vue/shared" "3.2.47" 397 | 398 | "@vue/shared@3.2.47": 399 | version "3.2.47" 400 | resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c" 401 | integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ== 402 | 403 | "@vueuse/core@^9.13.0": 404 | version "9.13.0" 405 | resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.13.0.tgz#2f69e66d1905c1e4eebc249a01759cf88ea00cf4" 406 | integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw== 407 | dependencies: 408 | "@types/web-bluetooth" "^0.0.16" 409 | "@vueuse/metadata" "9.13.0" 410 | "@vueuse/shared" "9.13.0" 411 | vue-demi "*" 412 | 413 | "@vueuse/metadata@9.13.0": 414 | version "9.13.0" 415 | resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff" 416 | integrity sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ== 417 | 418 | "@vueuse/shared@9.13.0": 419 | version "9.13.0" 420 | resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.13.0.tgz#089ff4cc4e2e7a4015e57a8f32e4b39d096353b9" 421 | integrity sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw== 422 | dependencies: 423 | vue-demi "*" 424 | 425 | algoliasearch@^4.0.0: 426 | version "4.14.3" 427 | resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.14.3.tgz#f02a77a4db17de2f676018938847494b692035e7" 428 | integrity sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg== 429 | dependencies: 430 | "@algolia/cache-browser-local-storage" "4.14.3" 431 | "@algolia/cache-common" "4.14.3" 432 | "@algolia/cache-in-memory" "4.14.3" 433 | "@algolia/client-account" "4.14.3" 434 | "@algolia/client-analytics" "4.14.3" 435 | "@algolia/client-common" "4.14.3" 436 | "@algolia/client-personalization" "4.14.3" 437 | "@algolia/client-search" "4.14.3" 438 | "@algolia/logger-common" "4.14.3" 439 | "@algolia/logger-console" "4.14.3" 440 | "@algolia/requester-browser-xhr" "4.14.3" 441 | "@algolia/requester-common" "4.14.3" 442 | "@algolia/requester-node-http" "4.14.3" 443 | "@algolia/transporter" "4.14.3" 444 | 445 | anchor-markdown-header@^0.6.0: 446 | version "0.6.0" 447 | resolved "https://registry.yarnpkg.com/anchor-markdown-header/-/anchor-markdown-header-0.6.0.tgz#908f2031281766f44ac350380ca0de77ab7065b8" 448 | integrity sha512-v7HJMtE1X7wTpNFseRhxsY/pivP4uAJbidVhPT+yhz4i/vV1+qx371IXuV9V7bN6KjFtheLJxqaSm0Y/8neJTA== 449 | dependencies: 450 | emoji-regex "~10.1.0" 451 | 452 | ansi-colors@^4.1.1: 453 | version "4.1.3" 454 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" 455 | integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== 456 | 457 | ansi-sequence-parser@^1.1.0: 458 | version "1.1.0" 459 | resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz#4d790f31236ac20366b23b3916b789e1bde39aed" 460 | integrity sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ== 461 | 462 | bail@^1.0.0: 463 | version "1.0.5" 464 | resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" 465 | integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== 466 | 467 | body-scroll-lock@4.0.0-beta.0: 468 | version "4.0.0-beta.0" 469 | resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz#4f78789d10e6388115c0460cd6d7d4dd2bbc4f7e" 470 | integrity sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ== 471 | 472 | boolbase@^1.0.0: 473 | version "1.0.0" 474 | resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" 475 | integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== 476 | 477 | ccount@^1.0.0: 478 | version "1.1.0" 479 | resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" 480 | integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== 481 | 482 | character-entities-legacy@^1.0.0: 483 | version "1.1.4" 484 | resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" 485 | integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== 486 | 487 | character-entities@^1.0.0: 488 | version "1.2.4" 489 | resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" 490 | integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== 491 | 492 | character-reference-invalid@^1.0.0: 493 | version "1.1.4" 494 | resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" 495 | integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== 496 | 497 | cheerio-select@^1.5.0: 498 | version "1.6.0" 499 | resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" 500 | integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== 501 | dependencies: 502 | css-select "^4.3.0" 503 | css-what "^6.0.1" 504 | domelementtype "^2.2.0" 505 | domhandler "^4.3.1" 506 | domutils "^2.8.0" 507 | 508 | cheerio@1.0.0-rc.10: 509 | version "1.0.0-rc.10" 510 | resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" 511 | integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== 512 | dependencies: 513 | cheerio-select "^1.5.0" 514 | dom-serializer "^1.3.2" 515 | domhandler "^4.2.0" 516 | htmlparser2 "^6.1.0" 517 | parse5 "^6.0.1" 518 | parse5-htmlparser2-tree-adapter "^6.0.1" 519 | tslib "^2.2.0" 520 | 521 | commander@9.2.0: 522 | version "9.2.0" 523 | resolved "https://registry.yarnpkg.com/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9" 524 | integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w== 525 | 526 | commander@^6.1.0: 527 | version "6.2.1" 528 | resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" 529 | integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== 530 | 531 | css-select@^4.3.0: 532 | version "4.3.0" 533 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" 534 | integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== 535 | dependencies: 536 | boolbase "^1.0.0" 537 | css-what "^6.0.1" 538 | domhandler "^4.3.1" 539 | domutils "^2.8.0" 540 | nth-check "^2.0.1" 541 | 542 | css-what@^6.0.1: 543 | version "6.1.0" 544 | resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" 545 | integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== 546 | 547 | csstype@^2.6.8: 548 | version "2.6.21" 549 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" 550 | integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== 551 | 552 | debug@^4.0.0, debug@^4.3.4: 553 | version "4.3.4" 554 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 555 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 556 | dependencies: 557 | ms "2.1.2" 558 | 559 | doctoc@^2.2.0: 560 | version "2.2.1" 561 | resolved "https://registry.yarnpkg.com/doctoc/-/doctoc-2.2.1.tgz#83f6a6bf4df97defbe027c9a82d13091a138ffe2" 562 | integrity sha512-qNJ1gsuo7hH40vlXTVVrADm6pdg30bns/Mo7Nv1SxuXSM1bwF9b4xQ40a6EFT/L1cI+Yylbyi8MPI4G4y7XJzQ== 563 | dependencies: 564 | "@textlint/markdown-to-ast" "^12.1.1" 565 | anchor-markdown-header "^0.6.0" 566 | htmlparser2 "^7.2.0" 567 | minimist "^1.2.6" 568 | underscore "^1.13.2" 569 | update-section "^0.3.3" 570 | 571 | dom-serializer@^1.0.1, dom-serializer@^1.3.2: 572 | version "1.4.1" 573 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" 574 | integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== 575 | dependencies: 576 | domelementtype "^2.0.1" 577 | domhandler "^4.2.0" 578 | entities "^2.0.0" 579 | 580 | domelementtype@^2.0.1, domelementtype@^2.2.0: 581 | version "2.3.0" 582 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" 583 | integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== 584 | 585 | domhandler@^3.3.0: 586 | version "3.3.0" 587 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" 588 | integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== 589 | dependencies: 590 | domelementtype "^2.0.1" 591 | 592 | domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1: 593 | version "4.3.1" 594 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" 595 | integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== 596 | dependencies: 597 | domelementtype "^2.2.0" 598 | 599 | domutils@^2.4.2, domutils@^2.5.2, domutils@^2.8.0: 600 | version "2.8.0" 601 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" 602 | integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== 603 | dependencies: 604 | dom-serializer "^1.0.1" 605 | domelementtype "^2.2.0" 606 | domhandler "^4.2.0" 607 | 608 | emoji-regex@~10.1.0: 609 | version "10.1.0" 610 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.1.0.tgz#d50e383743c0f7a5945c47087295afc112e3cf66" 611 | integrity sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg== 612 | 613 | entities@^2.0.0: 614 | version "2.2.0" 615 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" 616 | integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== 617 | 618 | entities@^3.0.1: 619 | version "3.0.1" 620 | resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" 621 | integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== 622 | 623 | esbuild@^0.16.14: 624 | version "0.16.17" 625 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" 626 | integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== 627 | optionalDependencies: 628 | "@esbuild/android-arm" "0.16.17" 629 | "@esbuild/android-arm64" "0.16.17" 630 | "@esbuild/android-x64" "0.16.17" 631 | "@esbuild/darwin-arm64" "0.16.17" 632 | "@esbuild/darwin-x64" "0.16.17" 633 | "@esbuild/freebsd-arm64" "0.16.17" 634 | "@esbuild/freebsd-x64" "0.16.17" 635 | "@esbuild/linux-arm" "0.16.17" 636 | "@esbuild/linux-arm64" "0.16.17" 637 | "@esbuild/linux-ia32" "0.16.17" 638 | "@esbuild/linux-loong64" "0.16.17" 639 | "@esbuild/linux-mips64el" "0.16.17" 640 | "@esbuild/linux-ppc64" "0.16.17" 641 | "@esbuild/linux-riscv64" "0.16.17" 642 | "@esbuild/linux-s390x" "0.16.17" 643 | "@esbuild/linux-x64" "0.16.17" 644 | "@esbuild/netbsd-x64" "0.16.17" 645 | "@esbuild/openbsd-x64" "0.16.17" 646 | "@esbuild/sunos-x64" "0.16.17" 647 | "@esbuild/win32-arm64" "0.16.17" 648 | "@esbuild/win32-ia32" "0.16.17" 649 | "@esbuild/win32-x64" "0.16.17" 650 | 651 | escape-goat@^3.0.0: 652 | version "3.0.0" 653 | resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-3.0.0.tgz#e8b5fb658553fe8a3c4959c316c6ebb8c842b19c" 654 | integrity sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw== 655 | 656 | escape-string-regexp@^4.0.0: 657 | version "4.0.0" 658 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 659 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 660 | 661 | esm@^3.2.25: 662 | version "3.2.25" 663 | resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" 664 | integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== 665 | 666 | estree-walker@^2.0.2: 667 | version "2.0.2" 668 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" 669 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== 670 | 671 | extend@^3.0.0: 672 | version "3.0.2" 673 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 674 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 675 | 676 | fault@^1.0.0: 677 | version "1.0.4" 678 | resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" 679 | integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== 680 | dependencies: 681 | format "^0.2.0" 682 | 683 | format@^0.2.0: 684 | version "0.2.2" 685 | resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" 686 | integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== 687 | 688 | fsevents@~2.3.2: 689 | version "2.3.2" 690 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 691 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 692 | 693 | function-bind@^1.1.1: 694 | version "1.1.1" 695 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 696 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 697 | 698 | has@^1.0.3: 699 | version "1.0.3" 700 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 701 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 702 | dependencies: 703 | function-bind "^1.1.1" 704 | 705 | htmlparser2@^5.0.0: 706 | version "5.0.1" 707 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7" 708 | integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ== 709 | dependencies: 710 | domelementtype "^2.0.1" 711 | domhandler "^3.3.0" 712 | domutils "^2.4.2" 713 | entities "^2.0.0" 714 | 715 | htmlparser2@^6.1.0: 716 | version "6.1.0" 717 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" 718 | integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== 719 | dependencies: 720 | domelementtype "^2.0.1" 721 | domhandler "^4.0.0" 722 | domutils "^2.5.2" 723 | entities "^2.0.0" 724 | 725 | htmlparser2@^7.2.0: 726 | version "7.2.0" 727 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5" 728 | integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog== 729 | dependencies: 730 | domelementtype "^2.0.1" 731 | domhandler "^4.2.2" 732 | domutils "^2.8.0" 733 | entities "^3.0.1" 734 | 735 | is-alphabetical@^1.0.0: 736 | version "1.0.4" 737 | resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" 738 | integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== 739 | 740 | is-alphanumerical@^1.0.0: 741 | version "1.0.4" 742 | resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" 743 | integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== 744 | dependencies: 745 | is-alphabetical "^1.0.0" 746 | is-decimal "^1.0.0" 747 | 748 | is-buffer@^2.0.0: 749 | version "2.0.5" 750 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" 751 | integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== 752 | 753 | is-core-module@^2.9.0: 754 | version "2.11.0" 755 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" 756 | integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== 757 | dependencies: 758 | has "^1.0.3" 759 | 760 | is-decimal@^1.0.0: 761 | version "1.0.4" 762 | resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" 763 | integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== 764 | 765 | is-hexadecimal@^1.0.0: 766 | version "1.0.4" 767 | resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" 768 | integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== 769 | 770 | is-plain-obj@^2.0.0: 771 | version "2.1.0" 772 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 773 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 774 | 775 | jsonc-parser@^3.2.0: 776 | version "3.2.0" 777 | resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" 778 | integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== 779 | 780 | juice@^8.0.0: 781 | version "8.1.0" 782 | resolved "https://registry.yarnpkg.com/juice/-/juice-8.1.0.tgz#4ea23362522fe06418229943237ee3751a4fca70" 783 | integrity sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA== 784 | dependencies: 785 | cheerio "1.0.0-rc.10" 786 | commander "^6.1.0" 787 | mensch "^0.3.4" 788 | slick "^1.12.2" 789 | web-resource-inliner "^6.0.1" 790 | 791 | longest-streak@^2.0.0: 792 | version "2.0.4" 793 | resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" 794 | integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== 795 | 796 | magic-string@^0.25.7: 797 | version "0.25.9" 798 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" 799 | integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== 800 | dependencies: 801 | sourcemap-codec "^1.4.8" 802 | 803 | markdown-it-footnote@^3.0.3: 804 | version "3.0.3" 805 | resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8" 806 | integrity sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w== 807 | 808 | markdown-it-mathjax3@^4.3.2: 809 | version "4.3.2" 810 | resolved "https://registry.yarnpkg.com/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz#1e34aa86f8560b283fd283008334adc2d6b05a37" 811 | integrity sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w== 812 | dependencies: 813 | juice "^8.0.0" 814 | mathjax-full "^3.2.0" 815 | 816 | markdown-table@^2.0.0: 817 | version "2.0.0" 818 | resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" 819 | integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== 820 | dependencies: 821 | repeat-string "^1.0.0" 822 | 823 | mathjax-full@^3.2.0: 824 | version "3.2.2" 825 | resolved "https://registry.yarnpkg.com/mathjax-full/-/mathjax-full-3.2.2.tgz#43f02e55219db393030985d2b6537ceae82f1fa7" 826 | integrity sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w== 827 | dependencies: 828 | esm "^3.2.25" 829 | mhchemparser "^4.1.0" 830 | mj-context-menu "^0.6.1" 831 | speech-rule-engine "^4.0.6" 832 | 833 | mdast-util-find-and-replace@^1.1.0: 834 | version "1.1.1" 835 | resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz#b7db1e873f96f66588c321f1363069abf607d1b5" 836 | integrity sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA== 837 | dependencies: 838 | escape-string-regexp "^4.0.0" 839 | unist-util-is "^4.0.0" 840 | unist-util-visit-parents "^3.0.0" 841 | 842 | mdast-util-footnote@^0.1.0: 843 | version "0.1.7" 844 | resolved "https://registry.yarnpkg.com/mdast-util-footnote/-/mdast-util-footnote-0.1.7.tgz#4b226caeab4613a3362c144c94af0fdd6f7e0ef0" 845 | integrity sha512-QxNdO8qSxqbO2e3m09KwDKfWiLgqyCurdWTQ198NpbZ2hxntdc+VKS4fDJCmNWbAroUdYnSthu+XbZ8ovh8C3w== 846 | dependencies: 847 | mdast-util-to-markdown "^0.6.0" 848 | micromark "~2.11.0" 849 | 850 | mdast-util-from-markdown@^0.8.0: 851 | version "0.8.5" 852 | resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c" 853 | integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ== 854 | dependencies: 855 | "@types/mdast" "^3.0.0" 856 | mdast-util-to-string "^2.0.0" 857 | micromark "~2.11.0" 858 | parse-entities "^2.0.0" 859 | unist-util-stringify-position "^2.0.0" 860 | 861 | mdast-util-frontmatter@^0.2.0: 862 | version "0.2.0" 863 | resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-0.2.0.tgz#8bd5cd55e236c03e204a036f7372ebe9e6748240" 864 | integrity sha512-FHKL4w4S5fdt1KjJCwB0178WJ0evnyyQr5kXTM3wrOVpytD0hrkvd+AOOjU9Td8onOejCkmZ+HQRT3CZ3coHHQ== 865 | dependencies: 866 | micromark-extension-frontmatter "^0.2.0" 867 | 868 | mdast-util-gfm-autolink-literal@^0.1.0, mdast-util-gfm-autolink-literal@^0.1.3: 869 | version "0.1.3" 870 | resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz#9c4ff399c5ddd2ece40bd3b13e5447d84e385fb7" 871 | integrity sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A== 872 | dependencies: 873 | ccount "^1.0.0" 874 | mdast-util-find-and-replace "^1.1.0" 875 | micromark "^2.11.3" 876 | 877 | mdast-util-gfm-strikethrough@^0.2.0: 878 | version "0.2.3" 879 | resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz#45eea337b7fff0755a291844fbea79996c322890" 880 | integrity sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA== 881 | dependencies: 882 | mdast-util-to-markdown "^0.6.0" 883 | 884 | mdast-util-gfm-table@^0.1.0: 885 | version "0.1.6" 886 | resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz#af05aeadc8e5ee004eeddfb324b2ad8c029b6ecf" 887 | integrity sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ== 888 | dependencies: 889 | markdown-table "^2.0.0" 890 | mdast-util-to-markdown "~0.6.0" 891 | 892 | mdast-util-gfm-task-list-item@^0.1.0: 893 | version "0.1.6" 894 | resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz#70c885e6b9f543ddd7e6b41f9703ee55b084af10" 895 | integrity sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A== 896 | dependencies: 897 | mdast-util-to-markdown "~0.6.0" 898 | 899 | mdast-util-gfm@^0.1.0: 900 | version "0.1.2" 901 | resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz#8ecddafe57d266540f6881f5c57ff19725bd351c" 902 | integrity sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ== 903 | dependencies: 904 | mdast-util-gfm-autolink-literal "^0.1.0" 905 | mdast-util-gfm-strikethrough "^0.2.0" 906 | mdast-util-gfm-table "^0.1.0" 907 | mdast-util-gfm-task-list-item "^0.1.0" 908 | mdast-util-to-markdown "^0.6.1" 909 | 910 | mdast-util-to-markdown@^0.6.0, mdast-util-to-markdown@^0.6.1, mdast-util-to-markdown@~0.6.0: 911 | version "0.6.5" 912 | resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz#b33f67ca820d69e6cc527a93d4039249b504bebe" 913 | integrity sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ== 914 | dependencies: 915 | "@types/unist" "^2.0.0" 916 | longest-streak "^2.0.0" 917 | mdast-util-to-string "^2.0.0" 918 | parse-entities "^2.0.0" 919 | repeat-string "^1.0.0" 920 | zwitch "^1.0.0" 921 | 922 | mdast-util-to-string@^2.0.0: 923 | version "2.0.0" 924 | resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" 925 | integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== 926 | 927 | mensch@^0.3.4: 928 | version "0.3.4" 929 | resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd" 930 | integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== 931 | 932 | mhchemparser@^4.1.0: 933 | version "4.1.1" 934 | resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.1.1.tgz#a2142fdab37a02ec8d1b48a445059287790becd5" 935 | integrity sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA== 936 | 937 | micromark-extension-footnote@^0.3.0: 938 | version "0.3.2" 939 | resolved "https://registry.yarnpkg.com/micromark-extension-footnote/-/micromark-extension-footnote-0.3.2.tgz#129b74ef4920ce96719b2c06102ee7abb2b88a20" 940 | integrity sha512-gr/BeIxbIWQoUm02cIfK7mdMZ/fbroRpLsck4kvFtjbzP4yi+OPVbnukTc/zy0i7spC2xYE/dbX1Sur8BEDJsQ== 941 | dependencies: 942 | micromark "~2.11.0" 943 | 944 | micromark-extension-frontmatter@^0.2.0: 945 | version "0.2.2" 946 | resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-0.2.2.tgz#61b8e92e9213e1d3c13f5a59e7862f5ca98dfa53" 947 | integrity sha512-q6nPLFCMTLtfsctAuS0Xh4vaolxSFUWUWR6PZSrXXiRy+SANGllpcqdXFv2z07l0Xz/6Hl40hK0ffNCJPH2n1A== 948 | dependencies: 949 | fault "^1.0.0" 950 | 951 | micromark-extension-gfm-autolink-literal@~0.5.0: 952 | version "0.5.7" 953 | resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz#53866c1f0c7ef940ae7ca1f72c6faef8fed9f204" 954 | integrity sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw== 955 | dependencies: 956 | micromark "~2.11.3" 957 | 958 | micromark-extension-gfm-strikethrough@~0.6.5: 959 | version "0.6.5" 960 | resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz#96cb83356ff87bf31670eefb7ad7bba73e6514d1" 961 | integrity sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw== 962 | dependencies: 963 | micromark "~2.11.0" 964 | 965 | micromark-extension-gfm-table@~0.4.0: 966 | version "0.4.3" 967 | resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz#4d49f1ce0ca84996c853880b9446698947f1802b" 968 | integrity sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA== 969 | dependencies: 970 | micromark "~2.11.0" 971 | 972 | micromark-extension-gfm-tagfilter@~0.3.0: 973 | version "0.3.0" 974 | resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz#d9f26a65adee984c9ccdd7e182220493562841ad" 975 | integrity sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q== 976 | 977 | micromark-extension-gfm-task-list-item@~0.3.0: 978 | version "0.3.3" 979 | resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz#d90c755f2533ed55a718129cee11257f136283b8" 980 | integrity sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ== 981 | dependencies: 982 | micromark "~2.11.0" 983 | 984 | micromark-extension-gfm@^0.3.0: 985 | version "0.3.3" 986 | resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz#36d1a4c089ca8bdfd978c9bd2bf1a0cb24e2acfe" 987 | integrity sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A== 988 | dependencies: 989 | micromark "~2.11.0" 990 | micromark-extension-gfm-autolink-literal "~0.5.0" 991 | micromark-extension-gfm-strikethrough "~0.6.5" 992 | micromark-extension-gfm-table "~0.4.0" 993 | micromark-extension-gfm-tagfilter "~0.3.0" 994 | micromark-extension-gfm-task-list-item "~0.3.0" 995 | 996 | micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: 997 | version "2.11.4" 998 | resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" 999 | integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA== 1000 | dependencies: 1001 | debug "^4.0.0" 1002 | parse-entities "^2.0.0" 1003 | 1004 | mime@^2.4.6: 1005 | version "2.6.0" 1006 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" 1007 | integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== 1008 | 1009 | minimist@^1.2.6: 1010 | version "1.2.8" 1011 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" 1012 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 1013 | 1014 | mj-context-menu@^0.6.1: 1015 | version "0.6.1" 1016 | resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz#a043c5282bf7e1cf3821de07b13525ca6f85aa69" 1017 | integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA== 1018 | 1019 | ms@2.1.2: 1020 | version "2.1.2" 1021 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 1022 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 1023 | 1024 | nanoid@^3.3.4: 1025 | version "3.3.4" 1026 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 1027 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 1028 | 1029 | node-fetch@^2.6.0: 1030 | version "2.6.9" 1031 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" 1032 | integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== 1033 | dependencies: 1034 | whatwg-url "^5.0.0" 1035 | 1036 | nth-check@^2.0.1: 1037 | version "2.1.1" 1038 | resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" 1039 | integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== 1040 | dependencies: 1041 | boolbase "^1.0.0" 1042 | 1043 | parse-entities@^2.0.0: 1044 | version "2.0.0" 1045 | resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" 1046 | integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== 1047 | dependencies: 1048 | character-entities "^1.0.0" 1049 | character-entities-legacy "^1.0.0" 1050 | character-reference-invalid "^1.0.0" 1051 | is-alphanumerical "^1.0.0" 1052 | is-decimal "^1.0.0" 1053 | is-hexadecimal "^1.0.0" 1054 | 1055 | parse5-htmlparser2-tree-adapter@^6.0.1: 1056 | version "6.0.1" 1057 | resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" 1058 | integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== 1059 | dependencies: 1060 | parse5 "^6.0.1" 1061 | 1062 | parse5@^6.0.1: 1063 | version "6.0.1" 1064 | resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" 1065 | integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== 1066 | 1067 | path-parse@^1.0.7: 1068 | version "1.0.7" 1069 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 1070 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 1071 | 1072 | picocolors@^1.0.0: 1073 | version "1.0.0" 1074 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 1075 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 1076 | 1077 | postcss@^8.1.10, postcss@^8.4.21: 1078 | version "8.4.21" 1079 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" 1080 | integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== 1081 | dependencies: 1082 | nanoid "^3.3.4" 1083 | picocolors "^1.0.0" 1084 | source-map-js "^1.0.2" 1085 | 1086 | preact@^10.0.0: 1087 | version "10.13.0" 1088 | resolved "https://registry.yarnpkg.com/preact/-/preact-10.13.0.tgz#f8bd3cf257a4dbe41da71a52131b79916d4ca89d" 1089 | integrity sha512-ERdIdUpR6doqdaSIh80hvzebHB7O6JxycOhyzAeLEchqOq/4yueslQbfnPwXaNhAYacFTyCclhwkEbOumT0tHw== 1090 | 1091 | remark-footnotes@^3.0.0: 1092 | version "3.0.0" 1093 | resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-3.0.0.tgz#5756b56f8464fa7ed80dbba0c966136305d8cb8d" 1094 | integrity sha512-ZssAvH9FjGYlJ/PBVKdSmfyPc3Cz4rTWgZLI4iE/SX8Nt5l3o3oEjv3wwG5VD7xOjktzdwp5coac+kJV9l4jgg== 1095 | dependencies: 1096 | mdast-util-footnote "^0.1.0" 1097 | micromark-extension-footnote "^0.3.0" 1098 | 1099 | remark-frontmatter@^3.0.0: 1100 | version "3.0.0" 1101 | resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-3.0.0.tgz#ca5d996361765c859bd944505f377d6b186a6ec6" 1102 | integrity sha512-mSuDd3svCHs+2PyO29h7iijIZx4plX0fheacJcAoYAASfgzgVIcXGYSq9GFyYocFLftQs8IOmmkgtOovs6d4oA== 1103 | dependencies: 1104 | mdast-util-frontmatter "^0.2.0" 1105 | micromark-extension-frontmatter "^0.2.0" 1106 | 1107 | remark-gfm@^1.0.0: 1108 | version "1.0.0" 1109 | resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-1.0.0.tgz#9213643001be3f277da6256464d56fd28c3b3c0d" 1110 | integrity sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA== 1111 | dependencies: 1112 | mdast-util-gfm "^0.1.0" 1113 | micromark-extension-gfm "^0.3.0" 1114 | 1115 | remark-parse@^9.0.0: 1116 | version "9.0.0" 1117 | resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640" 1118 | integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw== 1119 | dependencies: 1120 | mdast-util-from-markdown "^0.8.0" 1121 | 1122 | repeat-string@^1.0.0: 1123 | version "1.6.1" 1124 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 1125 | integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== 1126 | 1127 | resolve@^1.22.1: 1128 | version "1.22.1" 1129 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" 1130 | integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== 1131 | dependencies: 1132 | is-core-module "^2.9.0" 1133 | path-parse "^1.0.7" 1134 | supports-preserve-symlinks-flag "^1.0.0" 1135 | 1136 | rollup@^3.10.0: 1137 | version "3.17.2" 1138 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.17.2.tgz#a4ecd29c488672a0606e41ef57474fad715750a9" 1139 | integrity sha512-qMNZdlQPCkWodrAZ3qnJtvCAl4vpQ8q77uEujVCCbC/6CLB7Lcmvjq7HyiOSnf4fxTT9XgsE36oLHJBH49xjqA== 1140 | optionalDependencies: 1141 | fsevents "~2.3.2" 1142 | 1143 | shiki@^0.14.1: 1144 | version "0.14.1" 1145 | resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.1.tgz#9fbe082d0a8aa2ad63df4fbf2ee11ec924aa7ee1" 1146 | integrity sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw== 1147 | dependencies: 1148 | ansi-sequence-parser "^1.1.0" 1149 | jsonc-parser "^3.2.0" 1150 | vscode-oniguruma "^1.7.0" 1151 | vscode-textmate "^8.0.0" 1152 | 1153 | slick@^1.12.2: 1154 | version "1.12.2" 1155 | resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" 1156 | integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== 1157 | 1158 | source-map-js@^1.0.2: 1159 | version "1.0.2" 1160 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 1161 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 1162 | 1163 | source-map@^0.6.1: 1164 | version "0.6.1" 1165 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 1166 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 1167 | 1168 | sourcemap-codec@^1.4.8: 1169 | version "1.4.8" 1170 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" 1171 | integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== 1172 | 1173 | speech-rule-engine@^4.0.6: 1174 | version "4.0.7" 1175 | resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz#b655dacbad3dae04acc0f7665e26ef258397dd09" 1176 | integrity sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g== 1177 | dependencies: 1178 | commander "9.2.0" 1179 | wicked-good-xpath "1.3.0" 1180 | xmldom-sre "0.1.31" 1181 | 1182 | supports-preserve-symlinks-flag@^1.0.0: 1183 | version "1.0.0" 1184 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 1185 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 1186 | 1187 | tr46@~0.0.3: 1188 | version "0.0.3" 1189 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 1190 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 1191 | 1192 | traverse@^0.6.7: 1193 | version "0.6.7" 1194 | resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.7.tgz#46961cd2d57dd8706c36664acde06a248f1173fe" 1195 | integrity sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg== 1196 | 1197 | trough@^1.0.0: 1198 | version "1.0.5" 1199 | resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" 1200 | integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== 1201 | 1202 | tslib@^2.2.0: 1203 | version "2.5.0" 1204 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" 1205 | integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== 1206 | 1207 | underscore@^1.13.2: 1208 | version "1.13.6" 1209 | resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" 1210 | integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== 1211 | 1212 | unified@^9.2.2: 1213 | version "9.2.2" 1214 | resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" 1215 | integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== 1216 | dependencies: 1217 | bail "^1.0.0" 1218 | extend "^3.0.0" 1219 | is-buffer "^2.0.0" 1220 | is-plain-obj "^2.0.0" 1221 | trough "^1.0.0" 1222 | vfile "^4.0.0" 1223 | 1224 | unist-util-is@^4.0.0: 1225 | version "4.1.0" 1226 | resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" 1227 | integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== 1228 | 1229 | unist-util-stringify-position@^2.0.0: 1230 | version "2.0.3" 1231 | resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" 1232 | integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== 1233 | dependencies: 1234 | "@types/unist" "^2.0.2" 1235 | 1236 | unist-util-visit-parents@^3.0.0: 1237 | version "3.1.1" 1238 | resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" 1239 | integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== 1240 | dependencies: 1241 | "@types/unist" "^2.0.0" 1242 | unist-util-is "^4.0.0" 1243 | 1244 | update-section@^0.3.3: 1245 | version "0.3.3" 1246 | resolved "https://registry.yarnpkg.com/update-section/-/update-section-0.3.3.tgz#458f17820d37820dc60e20b86d94391b00123158" 1247 | integrity sha512-BpRZMZpgXLuTiKeiu7kK0nIPwGdyrqrs6EDSaXtjD/aQ2T+qVo9a5hRC3HN3iJjCMxNT/VxoLGQ7E/OzE5ucnw== 1248 | 1249 | valid-data-url@^3.0.0: 1250 | version "3.0.1" 1251 | resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f" 1252 | integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== 1253 | 1254 | vfile-message@^2.0.0: 1255 | version "2.0.4" 1256 | resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" 1257 | integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== 1258 | dependencies: 1259 | "@types/unist" "^2.0.0" 1260 | unist-util-stringify-position "^2.0.0" 1261 | 1262 | vfile@^4.0.0: 1263 | version "4.2.1" 1264 | resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" 1265 | integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== 1266 | dependencies: 1267 | "@types/unist" "^2.0.0" 1268 | is-buffer "^2.0.0" 1269 | unist-util-stringify-position "^2.0.0" 1270 | vfile-message "^2.0.0" 1271 | 1272 | vite@^4.1.3: 1273 | version "4.1.4" 1274 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.4.tgz#170d93bcff97e0ebc09764c053eebe130bfe6ca0" 1275 | integrity sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg== 1276 | dependencies: 1277 | esbuild "^0.16.14" 1278 | postcss "^8.4.21" 1279 | resolve "^1.22.1" 1280 | rollup "^3.10.0" 1281 | optionalDependencies: 1282 | fsevents "~2.3.2" 1283 | 1284 | vitepress@^1.0.0-alpha.47: 1285 | version "1.0.0-alpha.47" 1286 | resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-alpha.47.tgz#3488f23d6239757208d269773eff49cf9e898b09" 1287 | integrity sha512-vj+LOY0WJtKSk98HV4qqG6p4MofmF+C8yrWHiiw+GCMfr6C+610U5D7oD2OruroIafsON6F4nBDWGK8ZyGIpXQ== 1288 | dependencies: 1289 | "@docsearch/css" "^3.3.3" 1290 | "@docsearch/js" "^3.3.3" 1291 | "@vitejs/plugin-vue" "^4.0.0" 1292 | "@vue/devtools-api" "^6.5.0" 1293 | "@vueuse/core" "^9.13.0" 1294 | body-scroll-lock "4.0.0-beta.0" 1295 | shiki "^0.14.1" 1296 | vite "^4.1.3" 1297 | vue "^3.2.47" 1298 | 1299 | vscode-oniguruma@^1.7.0: 1300 | version "1.7.0" 1301 | resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" 1302 | integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== 1303 | 1304 | vscode-textmate@^8.0.0: 1305 | version "8.0.0" 1306 | resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d" 1307 | integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== 1308 | 1309 | vue-demi@*: 1310 | version "0.13.11" 1311 | resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99" 1312 | integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A== 1313 | 1314 | vue@^3.2.47: 1315 | version "3.2.47" 1316 | resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0" 1317 | integrity sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ== 1318 | dependencies: 1319 | "@vue/compiler-dom" "3.2.47" 1320 | "@vue/compiler-sfc" "3.2.47" 1321 | "@vue/runtime-dom" "3.2.47" 1322 | "@vue/server-renderer" "3.2.47" 1323 | "@vue/shared" "3.2.47" 1324 | 1325 | web-resource-inliner@^6.0.1: 1326 | version "6.0.1" 1327 | resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz#df0822f0a12028805fe80719ed52ab6526886e02" 1328 | integrity sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A== 1329 | dependencies: 1330 | ansi-colors "^4.1.1" 1331 | escape-goat "^3.0.0" 1332 | htmlparser2 "^5.0.0" 1333 | mime "^2.4.6" 1334 | node-fetch "^2.6.0" 1335 | valid-data-url "^3.0.0" 1336 | 1337 | webidl-conversions@^3.0.0: 1338 | version "3.0.1" 1339 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 1340 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 1341 | 1342 | whatwg-url@^5.0.0: 1343 | version "5.0.0" 1344 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 1345 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 1346 | dependencies: 1347 | tr46 "~0.0.3" 1348 | webidl-conversions "^3.0.0" 1349 | 1350 | wicked-good-xpath@1.3.0: 1351 | version "1.3.0" 1352 | resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c" 1353 | integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw== 1354 | 1355 | xmldom-sre@0.1.31: 1356 | version "0.1.31" 1357 | resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4" 1358 | integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw== 1359 | 1360 | zwitch@^1.0.0: 1361 | version "1.0.5" 1362 | resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" 1363 | integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== 1364 | --------------------------------------------------------------------------------