Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/pull_requests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ jobs:
GH_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node _build_scripts/update-config-versions.js
# Structural checks on the /e/ error-message redirector. No build needed,
# so it runs first and fails fast.
- name: Validate the error-message redirects
run: node _build_scripts/validate-redirects.js
- name: Build a dev version
run: yarn build-dev
- name: Validate links from the build.dev folder
Expand Down
4 changes: 4 additions & 0 deletions _build_scripts/validate-links-pr.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const runPRValidationFromBuildDev = async () => {
`/cloud`,
`/engram`,
`/contributor-guide`,
// Out of the site navigation, so nothing links down into it from the
// sections above and the crawler would otherwise never reach it.
`/errors`,
`/improve-your-cluster`,
]

const success = await validator.validateLinks(paths);
Expand Down
279 changes: 279 additions & 0 deletions _build_scripts/validate-redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/**
* Structural checks on the /e/<id> error-message redirector in netlify.toml.
*
* These invariants are load-bearing and none of them is enforced by anything
* else: Netlify does not validate the file, the Docusaurus build never reads
* it, and the link crawler only sees pages. They are also each one careless
* edit away from breaking, which is why this exists rather than a comment.
*
* Reads netlify.toml and the MDX under docs/. Needs no build and no network.
*
* node _build_scripts/validate-redirects.js
*/

const fs = require('fs')
const path = require('path')

const ROOT = path.resolve(__dirname, '..')
const TOML = path.join(ROOT, 'netlify.toml')
const BLOCK_MARKER = 'ERROR-MESSAGE REDIRECTOR'

const failures = []
const fail = (msg) => failures.push(msg)

const toml = fs.readFileSync(TOML, 'utf8')
const markerAt = toml.indexOf(BLOCK_MARKER)
if (markerAt === -1) {
console.error(`Could not find the "${BLOCK_MARKER}" block in netlify.toml.`)
process.exit(1)
}

// Comments are stripped first so that prose in the block (which discusses these
// very rules, and quotes them) can never be read as a rule or counted as one.
const block = toml
.slice(markerAt)
.split('\n')
.filter((line) => !/^[ \t]*#/.test(line))
.join('\n')

/**
* Parse [[redirects]] tables into plain objects.
*
* Deliberately NOT one regex over from/to/status in a fixed order with fixed
* spacing. TOML does not care about key order or whitespace, and this file is
* hand-edited, so a checker that only recognises today's formatting would go
* quietly blind on exactly the edit it exists to catch. Reads whatever keys are
* present, in any order, single- or double-quoted.
*/
const parseRedirectTables = (text) =>
text
.split(/\[\[redirects\]\]/)
.slice(1)
.map((chunk) => {
// A table ends at the next TOML header of any kind.
const body = chunk.split(/\n[ \t]*\[/)[0]
const KV = /^[ \t]*([A-Za-z_][A-Za-z0-9_-]*)[ \t]*=[ \t]*(?:"([^"]*)"|'([^']*)'|([^\s#]+))/gm
const entry = {}
for (const m of body.matchAll(KV)) {
entry[m[1]] = m[2] !== undefined ? m[2] : m[3] !== undefined ? m[3] : m[4]
}
return entry
})

const tables = parseRedirectTables(block)
const rules = tables.filter((t) => typeof t.from === 'string' && t.from.startsWith('/e/'))

// ---------------------------------------------------------------------------
// 0. THE PARSE ITSELF MUST BE COMPLETE.
//
// Every check below reasons over `rules`. If the parser silently drops a rule,
// each of them still passes, and the script reports success over a set it never
// examined. That is a worse outcome than having no checker at all, and it is
// the same failure shape as a link checker that validates zero files. So count
// the rules independently, as loosely as possible, and refuse to continue if
// the two numbers disagree.
// ---------------------------------------------------------------------------
const declared = (block.match(/^[ \t]*from[ \t]*=[ \t]*["']?\/e\//gm) || []).length
if (declared !== rules.length) {
console.error(
`\nnetlify.toml /e/ redirector: PARSE GAP.\n\n` +
` ${declared} rule(s) declare a /e/ source, but only ${rules.length} parsed.\n` +
` Every check in this script reasons over the parsed set, so it cannot be\n` +
` trusted until they agree. Fix the parser in ${path.basename(__filename)}\n` +
` rather than reformatting netlify.toml to suit it.\n`
)
process.exit(1)
}

if (rules.length === 0) fail('No /e/ redirect rules found.')

// Each rule must actually be a rule.
for (const r of rules) {
if (!r.to) fail(`"${r.from}" has no "to" target.`)
if (!r.status) fail(`"${r.from}" has no "status".`)
}

const splats = rules.filter((r) => r.from.includes('*'))
const specific = rules.filter((r) => !r.from.includes('*'))
const last = rules[rules.length - 1]

// 1. Exactly one catch-all, and it is the last /e/ rule. A rule appended after
// it would be unreachable, because the catch-all matches everything first.
if (splats.length !== 1) {
fail(`Expected exactly one splat rule in the /e/ block, found ${splats.length}: ${splats.map((r) => r.from).join(', ')}`)
} else if (splats[0].from !== '/e/*') {
fail(`The only splat should be "/e/*", found "${splats[0].from}".`)
} else if (last.from !== '/e/*') {
fail(`The catch-all must be the LAST /e/ rule, but "${last.from}" follows it. Rules after the catch-all can never match.`)
}

// 2. Status codes. A specific id maps to one meaning forever, so 301 and an
// indefinite browser cache are correct. The catch-all is the opposite: it
// fires for ids whose entry is not written yet, so its destination changes
// as soon as one is, and a cached 301 could never be corrected.
for (const r of specific) {
if (r.status !== '301') fail(`"${r.from}" should be status 301, found ${r.status}.`)
}
if (splats.length === 1 && splats[0].status !== '302') {
fail(`The "/e/*" catch-all should be status 302, found ${splats[0].status}. See the comment above it in netlify.toml.`)
}

// 3. Lowercase ids. Netlify matches paths case-sensitively, and messages print
// the id in mixed case for readability ("Dep004"), so a mixed-case rule here
// silently never fires.
for (const r of rules) {
if (r.from !== r.from.toLowerCase()) fail(`Redirect sources must be lowercase: "${r.from}".`)
}

// 4. No duplicate sources: a second rule for the same id is dead, because the
// first one always wins, and it reads as if it were in effect.
const seen = new Set()
for (const r of rules) {
if (seen.has(r.from)) fail(`Duplicate rule for "${r.from}" (the later one can never match).`)
seen.add(r.from)
}

// 5. No destination may carry a query string of its own.
//
// Netlify forwards an incoming query to the destination only when the
// destination has none. Measured with `netlify dev` and confirmed against
// production:
//
// to = "/errors/x#a" + ?clusterid=U -> /errors/x?clusterid=U#a
// to = "/errors/x?src=id#a" + ?clusterid=U -> /errors/x?src=id#a
//
// So a `?` here silently throws away the query the request arrived with,
// including the `?clusterid=<uuid>` Weaviate puts on these links, with no
// build error and no broken link to notice. The message id is added by
// netlify/edge-functions/error-link-src.ts instead, which is why every
// destination below can stay plain.
for (const r of rules) {
if (r.to && r.to.includes('?')) {
fail(
`"${r.from}" has a query string in its destination ("${r.to}"). ` +
`Netlify drops the reader's own query when the destination has one, ` +
`so this would discard ?clusterid=. Let error-link-src.ts add the id instead.`
)
}
}

// 6. At most one fragment. (Check 5 already covers the other half of this:
// "/x#a?b" is not a URL with a query, the "?b" is part of the fragment and
// never reaches the server, and it trips the "?" test above.)
for (const r of rules) {
if (!r.to) continue
const hashes = (r.to.match(/#/g) || []).length
if (hashes > 1) fail(`"${r.from}" has ${hashes} "#" in its destination ("${r.to}"). A URL has at most one fragment.`)
}

// 7. The edge function must be present and wired to /e/*.
//
// It is the only thing that puts `src=<id>` on the destination URL, since no
// `to` value here may carry a query string (check 5). Deleting or unwiring
// the file leaves the redirects working and the parameter silently gone, and
// nothing else would notice.
const EDGE_FN = path.join(ROOT, 'netlify', 'edge-functions', 'error-link-src.ts')
if (!fs.existsSync(EDGE_FN)) {
fail(
'netlify/edge-functions/error-link-src.ts is missing. It is what puts the ' +
'message id on the destination URL as ?src=<id>; without it the /e/ links ' +
'still resolve but the parameter is gone.'
)
} else {
// Comments are stripped first. The file's header quotes these settings
// verbatim, so a check run over the raw source passes on the documentation of
// a setting that has been deleted -- which is how this check failed its own
// negative test the first time it was written.
const fn = fs
.readFileSync(EDGE_FN, 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/^[ \t]*\/\/.*$/gm, '')
if (!/path:\s*["']\/e\/\*["']/.test(fn)) {
fail('error-link-src.ts no longer declares path: "/e/*", so it will not run for these rules.')
}
if (!/onError:\s*["']bypass["']/.test(fn)) {
fail(
'error-link-src.ts no longer sets onError: "bypass". Without it a thrown ' +
'error turns every /e/ link into a 500 instead of falling through to the ' +
'redirect rules below.'
)
}
}

// 8. Every destination must resolve to a real page AND a real anchor.
// Nothing downstream catches a bad fragment: the build only warns on broken
// anchors it finds in page links, and it never sees this file at all.
//
// Inside /errors the anchor must be an explicit {#anchor}: entries use
// overrides so that several ids can share one heading without the anchor
// being tied to the heading's wording. Elsewhere the rule points at pages
// the section does not own, so any anchor Docusaurus would render counts:
// an explicit {#anchor}, a heading's generated slug, or an <APITable> row id
// (the row's first cell, verbatim -- see src/components/APITable).
const docsPath = (route) => {
const rel = route.replace(/^\//, '')
for (const candidate of [`docs/${rel}.mdx`, `docs/${rel}.md`, `docs/${rel}/index.mdx`, `docs/${rel}/index.md`]) {
const full = path.join(ROOT, candidate)
if (fs.existsSync(full)) return full
}
return null
}

// github-slugger, as Docusaurus applies it to headings: lowercase, drop
// punctuation, spaces to hyphens. Inline code marks are dropped first since
// the slug is built from the rendered text.
const slugify = (text) =>
text
.replace(/`/g, '')
.toLowerCase()
.trim()
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-')

const anchorsIn = (body) => {
const anchors = new Set()
for (const m of body.matchAll(/\{#([^}\s]+)\}/g)) anchors.add(m[1])
for (const m of body.matchAll(/^#{1,6}[ \t]+(.+?)(?:[ \t]+\{#[^}]+\})?[ \t]*$/gm)) anchors.add(slugify(m[1]))
// APITable rows: <tr id={firstCellText}>, first cell verbatim minus code marks.
for (const m of body.matchAll(/^\|[ \t]*`?([^|`]+?)`?[ \t]*\|/gm)) anchors.add(m[1].trim())
return anchors
}

let checkedDestinations = 0
let checkedAnchors = 0
for (const r of rules) {
if (!r.to || r.to === '/errors' && r.status === '302') continue
if (!r.to.startsWith('/')) continue
if (r.to.startsWith('/e/')) continue // alias onto another rule, checked through that rule
const [route, fragment] = r.to.split('#')
const file = docsPath(route)
if (!file) {
fail(`"${r.from}" points at "${route}", which has no page under docs/.`)
continue
}
checkedDestinations++
if (!fragment) continue
checkedAnchors++
const body = fs.readFileSync(file, 'utf8')
const rel = path.relative(ROOT, file)
if (route.startsWith('/errors')) {
if (!body.includes(`{#${fragment}}`)) {
fail(`"${r.from}" points at "#${fragment}", which is not an explicit {#...} anchor in ${rel}.`)
}
} else if (!anchorsIn(body).has(fragment)) {
fail(`"${r.from}" points at "#${fragment}", which is not a heading slug, {#...} anchor, or APITable row id in ${rel}.`)
}
}

if (failures.length > 0) {
console.error(`\nnetlify.toml /e/ redirector: ${failures.length} problem(s)\n`)
for (const f of failures) console.error(` - ${f}`)
console.error('')
process.exit(1)
}

console.log(
`netlify.toml /e/ redirector OK: ${rules.length} rules parsed (all ${declared} declared), ` +
`${specific.length} specific (301), 1 catch-all (302, last), ` +
`${checkedDestinations} destinations verified to a real page, ${checkedAnchors} of them to a real anchor.`
)
2 changes: 2 additions & 0 deletions docs/deploy/configuration/env-vars/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import APITable from '@site/src/components/APITable';
| --- | --- | --- | --- |
| `ASYNC_INDEXING` | If set, Weaviate creates vector indexes asynchronously to the object creation process. This can be useful for importing large amounts of data. (default: `false`) | `boolean` | `false` |
| `AUTOSCHEMA_ENABLED` | Whether to infer the schema where necessary with the autoschema (default: `true`) | `boolean` | `true` |
| `BANNER_INTERVAL` | How often the startup banner is logged again while the node runs. The repeat banner draws its art from `https://weaviate.io/banner/v1.json` when it can be fetched, and its link carries `?clusterid=<uuid>` once the cluster has an id. The repeat and the fetch happen only when telemetry is enabled; `DISABLE_TELEMETRY=true` turns both off. Default: `24h`<br/>Added in `v1.40` | `string - duration` | `12h` |
| `CORS_ALLOW_HEADERS` | Value of the `Access-Control-Allow-Headers` response header on the REST API, which controls the request headers a browser may send cross-origin. The default is the long list of headers Weaviate itself reads, including `Content-Type`, `Authorization` and the per-provider API-key headers. Default: the built-in header list | `string - comma separated names` | `Content-Type, Authorization` |
| `CORS_ALLOW_METHODS` | Value of the `Access-Control-Allow-Methods` response header on the REST API, which controls the HTTP methods a browser may use cross-origin. Default: `*` | `string - comma separated names` | `GET, POST, OPTIONS` |
| `CORS_ALLOW_ORIGIN` | Value of the `Access-Control-Allow-Origin` response header on the REST API, which controls the origins a browser may call Weaviate from. Set this to reach Weaviate directly from browser code on a specific site. Default: `*` | `string` | `https://example.com` |
Expand All @@ -41,6 +42,7 @@ import APITable from '@site/src/components/APITable';
| `DEFAULT_VECTORIZER_MODULE` | Default vectorizer module - can be overridden by the vectorizer in the collection definition. | `string` | `text2vec-contextionary` |
| `API_BASED_MODULES_DISABLED` | Weaviate automatically enables the usage of all [API-based modules](../../../weaviate/model-providers/index.md#api-based). Set this variable to `true` in order to limit access and only allow specific modules through the [`ENABLE_MODULES`](#ENABLE_MODULES) variable. Default: `false`<br/> Added in `v1.33` | `boolean` | `true` |
| `DISABLE_LAZY_LOAD_SHARDS` | When `false`, enable lazy shard loading to improve mean time to recovery in multi-tenant deployments. **Deprecated in `v1.36.6`.** Use `LAZY_LOAD_SHARD_COUNT_THRESHOLD` and `LAZY_LOAD_SHARD_SIZE_THRESHOLD_GB` instead. Weaviate now auto-detects when lazy loading is needed per collection. | `string` | `false` |
| `DISABLE_STARTUP_BANNER` | Disable the banner Weaviate logs as its first entry on startup (`action=banner`, with the version and the link to [Improve your cluster](/improve-your-cluster)), and the repeat of it every `BANNER_INTERVAL`. The banner is an `info` entry, so `LOG_LEVEL=warning` or stricter hides it as well. Default: `false`<br/>Added in `v1.40` | `boolean` | `true` |
| `DISABLE_TELEMETRY` | Disable [telemetry](/deploy/configuration/telemetry.md) data collection | boolean | `false` |
| `DISK_USE_READONLY_PERCENTAGE` | If disk usage is higher than the given percentage all shards on the affected node will be marked as `READONLY`, meaning all future write requests will fail. See [Disk Pressure Warnings and Limits for details](/deploy/configuration/persistence.md#disk-pressure-warnings-and-limits). | `string - number` | `90` |
| `DISK_USE_WARNING_PERCENTAGE` | If disk usage is higher than the given percentage a warning will be logged by all shards on the affected node's disk. See [Disk Pressure Warnings and Limits for details](/deploy/configuration/persistence.md#disk-pressure-warnings-and-limits). | `string - number` | `80` |
Expand Down
14 changes: 14 additions & 0 deletions docs/deploy/configuration/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ time="2026-01-06T15:55:01Z" level=info action=authorize build_git_commit=5f0048c
- Debugging issues locally
- Easier visual scanning is preferred over machine parsing

### Messages with a docs link

A log entry whose cause has a documented fix carries a `docs_url` field. The link is a stable short id, `https://docs.weaviate.io/e/<id>`, that resolves to the entry for that message in the [error message reference](/errors), where the cause and the fix are on one page:

```json
{"action":"tenant_activation_lazy_load_shard","docs_url":"https://docs.weaviate.io/e/core-mem001","level":"error","msg":"loading shard \"t1\" failed: memory pressure: cannot init shard: not enough memory mappings","shard":"t1","time":"2026-08-25T09:53:26Z"}
```

The field is separate from the message, so alerts and filters that match on message text keep working, and in JSON logs you can select on it directly. The error returned to the client for the same failure carries the same link at the end of its message, as `(see https://docs.weaviate.io/e/core-mem001)`. Added in `v1.40`.

Weaviate also logs a banner as its first entry on startup, with the version and a link to [Improve your cluster](/improve-your-cluster), and repeats it every [`BANNER_INTERVAL`](./env-vars/index.md#BANNER_INTERVAL) (24 hours by default). The repeat banner fetches its art from `https://weaviate.io/banner/v1.json` and, once the cluster has an id, carries it on the link as `?clusterid=<uuid>`; both happen only while telemetry is enabled. Set [`DISABLE_STARTUP_BANNER`](./env-vars/index.md#DISABLE_STARTUP_BANNER) to turn the banner off entirely.

From the same point in startup, every `docs_url` and every link in an error message carries the cluster id the same way, so a docs page can tell which cluster the reader came from. A cluster with telemetry disabled never has an id, and no link carries one in the first seconds after start, before the cluster has elected a leader.

## Accessing Logs

Weaviate logs can be accessed using standard container logging commands. For example:
Expand Down
2 changes: 2 additions & 0 deletions docs/deploy/faqs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Consider every error message a clue to solving the mystery you're encountering.

## Common issues and solutions

Looking up one specific message? The [error message reference](/errors) lists Weaviate's error and warning messages by text and by message id, with the cause and the fix for each. Newer Weaviate versions link to it straight from the log entry, through a `docs_url` field, and from the error a client receives.

### The cluster is not accepting new information and there are disk space or `read-only` error messages in the logs.

<details>
Expand Down
Loading
Loading