Weighs the soul of incoming HTTP requests to stop AI crawlers

feat(lib/policy/expressions): add system load average to bot expression inputs (#766)

* feat(lib/policy/expressions): add system load average to bot expression inputs

This lets Anubis dynamically react to system load in order to
increase and decrease the required level of scrutiny. High load? More
scrutiny required. Low load? Less scrutiny required.

* docs: spell system correctly

Signed-off-by: Xe Iaso <me@xeiaso.net>

* Update metadata

check-spelling run (pull_request) for Xe/load-average

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>

* fix(default-config): don't enable low load average feature by default

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>

authored by

Xe Iaso and committed by
GitHub
4ea0add5 289c802a

+192 -31
+3
.github/actions/spelling/expect.txt
··· 120 120 gomod 121 121 goodbot 122 122 googlebot 123 + gopsutil 123 124 govulncheck 124 125 goyaml 125 126 GPG ··· 193 194 mojeekbot 194 195 mozilla 195 196 nbf 197 + nepeat 196 198 netsurf 197 199 nginx 198 200 nicksnyder ··· 259 261 Seo 260 262 setsebool 261 263 shellcheck 264 + shirou 262 265 Sidetrade 263 266 simprint 264 267 sitemap
+19
data/botPolicies.yaml
··· 74 74 weight: 75 75 adjust: 10 76 76 77 + ## System load based checks. 78 + # If the system is under high load, add weight. 79 + - name: high-load-average 80 + action: WEIGH 81 + expression: load_1m >= 10.0 # make sure to end the load comparison in a .0 82 + weight: 83 + adjust: 20 84 + 85 + ## If your backend service is running on the same operating system as Anubis, 86 + ## you can uncomment this rule to make the challenge easier when the system is 87 + ## under low load. 88 + ## 89 + ## If it is not, remove weight. 90 + # - name: low-load-average 91 + # action: WEIGH 92 + # expression: load_15m <= 4.0 # make sure to end the load comparison in a .0 93 + # weight: 94 + # adjust: -10 95 + 77 96 # Generic catchall rule 78 97 - name: generic-browser 79 98 user_agent_regex: >-
+1
docs/docs/CHANGELOG.md
··· 29 29 - Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751)) 30 30 - Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape 31 31 - The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes. 32 + - Added the ability for Anubis to dynamically take action [based on the system load average](./admin/configuration/expressions.mdx#using-the-system-load-average). 32 33 - Add translation for Traditional Chinese ([#759](https://github.com/TecharoHQ/anubis/pull/759)) 33 34 34 35 ### Potentially breaking changes
+50 -9
docs/docs/admin/configuration/expressions.mdx
··· 99 99 100 100 Anubis exposes the following variables to expressions: 101 101 102 - | Name | Type | Explanation | Example | 103 - | :-------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | 104 - | `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` | 105 - | `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` | 106 - | `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. | 107 - | `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` | 108 - | `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` | 109 - | `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` | 110 - | `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` | 102 + | Name | Type | Explanation | Example | 103 + | :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | 104 + | `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` | 105 + | `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` | 106 + | `load_1m` | `double` | The current system load average over the last one minute. This is useful for making [load-based checks](#using-the-system-load-average). | 107 + | `load_5m` | `double` | The current system load average over the last five minutes. This is useful for making [load-based checks](#using-the-system-load-average). | 108 + | `load_15m` | `double` | The current system load average over the last fifteen minutes. This is useful for making [load-based checks](#using-the-system-load-average). | 109 + | `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. | 110 + | `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` | 111 + | `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` | 112 + | `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` | 113 + | `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` | 111 114 112 115 Of note: in many languages when you look up a key in a map and there is nothing there, the language will return some "falsy" value like `undefined` in JavaScript, `None` in Python, or the zero value of the type in Go. In CEL, if you try to look up a value that does not exist, execution of the expression will fail and Anubis will return an error. 113 116 ··· 140 143 ``` 141 144 142 145 Anubis would return a challenge because all of those conditions are true. 146 + 147 + ### Using the system load average 148 + 149 + In Unix-like systems (such as Linux), every process on the system has to wait its turn to be able to run. This means that as more processes on the system are running, they need to wait longer to be able to execute. The [load average](<https://en.wikipedia.org/wiki/Load_(computing)>) represents the number of processes that want to be able to run but can't run yet. This metric isn't the most reliable to identify a cause, but is great at helping to identify symptoms. 150 + 151 + Anubis lets you use the system load average as an input to expressions so that you can make dynamic rules like "when the system is under a low amount of load, dial back the protection, but when it's under a lot of load, crank it up to the mix". This lets you get all of the blocking features of Anubis in the background but only really expose Anubis to users when the system is actively being attacked. 152 + 153 + This is best combined with the [weight](../policies.mdx#request-weight) and [threshold](./thresholds.mdx) systems so that you can have Anubis dynamically respond to attacks. Consider these rules in the default configuration file: 154 + 155 + ```yaml 156 + ## System load based checks. 157 + # If the system is under high load for the last minute, add weight. 158 + - name: high-load-average 159 + action: WEIGH 160 + expression: load_1m >= 10.0 # make sure to end the load comparison in a .0 161 + weight: 162 + adjust: 20 163 + 164 + # If it is not for the last 15 minutes, remove weight. 165 + - name: low-load-average 166 + action: WEIGH 167 + expression: load_15m <= 4.0 # make sure to end the load comparison in a .0 168 + weight: 169 + adjust: -10 170 + ``` 171 + 172 + This combination of rules makes Anubis dynamically react to the system load and only kick in when the system is under attack. 173 + 174 + Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example: 175 + 176 + | Kind | Core count | Load threshold | 177 + | --------: | :--------- | :------------- | 178 + | high load | 4 | `8.0` | 179 + | low load | 4 | `2.0` | 180 + | high load | 16 | `32.0` | 181 + | low load | 16 | `8` | 182 + 183 + Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero. 143 184 144 185 ## Functions exposed to Anubis expressions 145 186
+11 -8
go.mod
··· 19 19 github.com/prometheus/client_golang v1.22.0 20 20 github.com/redis/go-redis/v9 v9.11.0 21 21 github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a 22 + github.com/shirou/gopsutil/v4 v4.25.1 23 + github.com/testcontainers/testcontainers-go v0.37.0 22 24 go.etcd.io/bbolt v1.4.2 23 25 golang.org/x/net v0.41.0 24 26 golang.org/x/text v0.26.0 ··· 79 81 github.com/go-git/go-billy/v5 v5.6.2 // indirect 80 82 github.com/go-git/go-git/v5 v5.14.0 // indirect 81 83 github.com/go-jose/go-jose/v3 v3.0.4 // indirect 82 - github.com/go-logr/logr v1.4.2 // indirect 84 + github.com/go-logr/logr v1.4.3 // indirect 83 85 github.com/go-logr/stdr v1.2.2 // indirect 84 86 github.com/go-ole/go-ole v1.2.6 // indirect 85 87 github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect ··· 127 129 github.com/prometheus/procfs v0.15.1 // indirect 128 130 github.com/russross/blackfriday/v2 v2.1.0 // indirect 129 131 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 130 - github.com/shirou/gopsutil/v4 v4.25.1 // indirect 131 132 github.com/shopspring/decimal v1.4.0 // indirect 132 133 github.com/sirupsen/logrus v1.9.3 // indirect 133 134 github.com/skeema/knownhosts v1.3.1 // indirect ··· 138 139 github.com/suzuki-shunsuke/logrus-error v0.1.4 // indirect 139 140 github.com/suzuki-shunsuke/pinact v1.6.0 // indirect 140 141 github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 // indirect 141 - github.com/testcontainers/testcontainers-go v0.37.0 // indirect 142 142 github.com/tklauser/go-sysconf v0.3.12 // indirect 143 143 github.com/tklauser/numcpus v0.6.1 // indirect 144 144 github.com/ulikunitz/xz v0.5.12 // indirect ··· 149 149 gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect 150 150 go.opentelemetry.io/auto/sdk v1.1.0 // indirect 151 151 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 152 - go.opentelemetry.io/otel v1.35.0 // indirect 153 - go.opentelemetry.io/otel/metric v1.35.0 // indirect 154 - go.opentelemetry.io/otel/trace v1.35.0 // indirect 152 + go.opentelemetry.io/otel v1.37.0 // indirect 153 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect 154 + go.opentelemetry.io/otel/metric v1.37.0 // indirect 155 + go.opentelemetry.io/otel/sdk v1.37.0 // indirect 156 + go.opentelemetry.io/otel/trace v1.37.0 // indirect 157 + go.opentelemetry.io/proto/otlp v1.7.0 // indirect 155 158 go.yaml.in/yaml/v2 v2.4.2 // indirect 156 159 go.yaml.in/yaml/v3 v3.0.3 // indirect 157 160 golang.org/x/crypto v0.39.0 // indirect ··· 166 169 golang.org/x/tools v0.34.0 // indirect 167 170 golang.org/x/vuln v1.1.4 // indirect 168 171 golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 169 - google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect 170 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect 172 + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect 173 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect 171 174 google.golang.org/protobuf v1.36.6 // indirect 172 175 gopkg.in/warnings.v0 v0.1.2 // indirect 173 176 honnef.co/go/tools v0.6.1 // indirect
+30 -14
go.sum
··· 6 6 cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 7 7 dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 8 8 dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 9 + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 10 + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 9 11 github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= 10 12 github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= 11 13 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= ··· 146 148 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 147 149 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 148 150 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 149 - github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 150 - github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 151 + github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 152 + github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 151 153 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 152 154 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 153 155 github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= ··· 214 216 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= 215 217 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= 216 218 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= 219 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 220 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 217 221 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 218 222 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 219 223 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= ··· 351 355 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 352 356 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 353 357 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 358 + github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 359 + github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 354 360 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 355 361 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 356 362 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= ··· 395 401 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 396 402 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 397 403 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 398 - go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 399 - go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 400 - go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 401 - go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 402 - go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 403 - go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 404 + go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 405 + go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 406 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= 407 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= 408 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 409 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 410 + go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 411 + go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 412 + go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 413 + go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 404 414 go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 405 415 go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 406 - go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 407 - go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 416 + go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 417 + go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 418 + go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= 419 + go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= 408 420 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 409 421 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 410 422 go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= ··· 494 506 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 495 507 golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= 496 508 golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 509 + golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 510 + golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 497 511 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 498 512 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 499 513 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= ··· 511 525 golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 512 526 golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 513 527 golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 514 - google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= 515 - google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= 516 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= 517 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 528 + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= 529 + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= 530 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= 531 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 518 532 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 519 533 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 520 534 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= ··· 533 547 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 534 548 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 535 549 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 550 + gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 551 + gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 536 552 honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= 537 553 honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= 538 554 k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
+6
lib/policy/celchecker.go
··· 73 73 return expressions.URLValues{Values: cr.URL.Query()}, true 74 74 case "headers": 75 75 return expressions.HTTPHeaders{Header: cr.Header}, true 76 + case "load_1m": 77 + return expressions.Load1(), true 78 + case "load_5m": 79 + return expressions.Load5(), true 80 + case "load_15m": 81 + return expressions.Load15(), true 76 82 default: 77 83 return nil, false 78 84 }
+3
lib/policy/expressions/environment.go
··· 23 23 cel.Variable("path", cel.StringType), 24 24 cel.Variable("query", cel.MapType(cel.StringType, cel.StringType)), 25 25 cel.Variable("headers", cel.MapType(cel.StringType, cel.StringType)), 26 + cel.Variable("load_1m", cel.DoubleType), 27 + cel.Variable("load_5m", cel.DoubleType), 28 + cel.Variable("load_15m", cel.DoubleType), 26 29 ) 27 30 } 28 31
+69
lib/policy/expressions/loadavg.go
··· 1 + package expressions 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "sync" 7 + "time" 8 + 9 + "github.com/shirou/gopsutil/v4/load" 10 + ) 11 + 12 + type loadAvg struct { 13 + lock sync.RWMutex 14 + data *load.AvgStat 15 + } 16 + 17 + func (l *loadAvg) updateThread(ctx context.Context) { 18 + ticker := time.NewTicker(15 * time.Second) 19 + defer ticker.Stop() 20 + 21 + l.update() 22 + 23 + for { 24 + select { 25 + case <-ticker.C: 26 + l.update() 27 + case <-ctx.Done(): 28 + return 29 + } 30 + } 31 + } 32 + 33 + func (l *loadAvg) update() { 34 + l.lock.Lock() 35 + defer l.lock.Unlock() 36 + 37 + var err error 38 + l.data, err = load.Avg() 39 + if err != nil { 40 + slog.Debug("can't get load average", "err", err) 41 + } 42 + } 43 + 44 + var ( 45 + globalLoadAvg *loadAvg 46 + ) 47 + 48 + func init() { 49 + globalLoadAvg = &loadAvg{} 50 + go globalLoadAvg.updateThread(context.Background()) 51 + } 52 + 53 + func Load1() float64 { 54 + globalLoadAvg.lock.RLock() 55 + defer globalLoadAvg.lock.RUnlock() 56 + return globalLoadAvg.data.Load1 57 + } 58 + 59 + func Load5() float64 { 60 + globalLoadAvg.lock.RLock() 61 + defer globalLoadAvg.lock.RUnlock() 62 + return globalLoadAvg.data.Load5 63 + } 64 + 65 + func Load15() float64 { 66 + globalLoadAvg.lock.RLock() 67 + defer globalLoadAvg.lock.RUnlock() 68 + return globalLoadAvg.data.Load15 69 + }