For the complete documentation index, see llms.txt. This page is also available as Markdown.

Configuration

Declaring services with .dstl8.yaml, excluding files with .dstl8ignore, and language coverage

Two optional files at the repo root configure extraction: .dstl8.yaml declares the services a repo builds, and .dstl8ignore excludes paths. Neither is required — service detection and the built-in excludes cover the common cases.

Declaring services: .dstl8.yaml

Services are how log sites map to what's actually running: a pattern link that says service checkout emits this line from payments/client.go:212 is only as good as the repo→service mapping. The extractor detects services automatically, first match wins per service name, in this order:

Precedence

Source

Where it looks

source tag in the artifact

1

.dstl8.yaml

services: stanza at the repo root

dstl8_yaml

2

Helm

charts/**/values.yaml

helm

3

Kubernetes manifests

k8s/**/*.y{a,}ml, deploy/**/*.y{a,}mlapp.kubernetes.io/name labels, app: labels, Deployment names

k8s

4

Compose / Procfile

docker-compose*.y{a,}ml service keys, Procfile process names

compose

5

Fallback

the repo's short name as the single service (only when nothing else matched)

inferred

Every service in the artifact carries its source tag, so Möbius knows how much to trust the mapping. If detection gets it wrong — or a monorepo builds several services from different subtrees — declare them explicitly:

# .dstl8.yaml
services:
  - name: checkout

With no paths, the whole repo maps to the service. This is mainly useful when the deployed service name differs from the repo name and there are no manifests to detect it from.

# .dstl8.yaml
services:
  - name: checkout
    paths: ["cmd/checkout/**", "payments/**"]
  - name: checkout-worker
    paths: ["cmd/worker/**"]
  - name: admin-api
    paths: ["services/admin/**"]

paths are glob patterns relative to the repo root; a log site maps to the service whose globs cover its file.

name is required for every entry. A malformed .dstl8.yaml is a hard error — it's the explicit override, so it fails loudly instead of silently falling back to detection. The --service name=glob CLI flag sits at the same trust tier: it wins over any detected or declared mapping with the same name, and other detected services are kept.

Excluding files: .dstl8ignore

The extractor never reads a file you've told git to ignore, and .dstl8ignore excludes more on top. Exclusions are evaluated in this order:

  1. .gitignore — always respected: files are enumerated with git ls-files, so only git-tracked files are ever scanned. Untracked and ignored files never enter the pipeline.

  2. .dstl8ignore — extra excludes, at the repo root.

  3. Built-in excludes (below).

  4. Test sources (below) — unless you pass --include-tests.

  5. --exclude flags on build/push.

.dstl8ignore uses gitignore pattern syntax:

  • one pattern per line; blank lines are skipped

  • # starts a comment line

  • ! re-includes something a previous pattern excluded — the last matching rule wins

  • a trailing / matches directories only (and everything beneath them)

  • *, ?, and [...] match within one path segment; ** crosses segments

  • a pattern containing / is anchored to the repo root; a bare name matches at any depth

A worked example — keeping generated code out of the graph:

Excluded files disappear from the artifact entirely — they're not in the file inventory, and their log sites are never extracted. Verify the effect with dstl8 graph build --dry-run | jq '.files[].path'.

Outside a git repo (e.g. an exported source tarball), git ls-files isn't available and .gitignore is not parsed — the walk covers everything except .git. In that situation .dstl8ignore and --exclude are your only carve-outs.

Built-in excludes

Applied after .dstl8ignore, always on:

  • Directories at any depth: vendor/, node_modules/, third_party/, dist/, build/, testdata/

  • Generated/minified file suffixes: *.min.js, *_pb.go, *.gen.go

  • Files larger than 1 MB (counted in stats.skipped_files)

  • Binary files (null byte in the first 8 KB)

There's no way to re-include a built-in exclude — vendored and generated code stays out by design.

Test sources

Test files are excluded by default. A log statement inside a test never runs in production, so a runtime pattern that matches one is a false attribution — Dstl8 would place a live error in a fixture instead of in the code that actually emitted it.

Detection is by path, following each language's conventions:

Excluded

Directories (any depth)

test/, tests/, spec/, specs/, __tests__/, __mocks__/, test_suite/ — this also covers Maven/Gradle's src/test/…

Go

*_test.go

Python

test_*.py, *_test.py, *_tests.py

JavaScript / TypeScript

*.test.js, *.spec.js and the .jsx, .mjs, .cjs, .ts, .tsx variants

Ruby

*_spec.rb, *_test.rb

Java / Kotlin

*Test.java, *Tests.java, *Test.kt, *Tests.kt

C#

*Test.cs, *Tests.cs

PHP

*Test.php, *Tests.php

Rust

*_test.rs, plus the crate's tests/ directory

Elixir

*_test.ex, *_test.exs

Files that merely look like tests are kept — internal/testing/harness.go, pkg/attestation/verify.go and services/latest/handler.go all stay in the graph.

Tests inside a production file (Rust's #[cfg(test)] mod tests, a nested JUnit class) can't be detected by path and are still indexed. If those produce noisy matches, exclude the file with .dstl8ignore.

dstl8 graph build reports the count so the effect is never invisible:

Pass --include-tests on build/push to index them anyway — useful if your tests emit the same log statements you're trying to trace, or if you want deploy diffs to include test-only changes.

Language coverage

Every text file that survives the walk appears in the file inventory (path, language, line count, content hash). What else is extracted depends on the language:

Languages
Parser
Log sites
Symbols & imports

Go

Go compiler AST

Yes (confidence: extracted)

Yes — functions, methods, types; import edges; go.mod deps

Python, JavaScript, TypeScript, TSX, Java, Ruby, C#, Rust, Kotlin, PHP

tree-sitter grammars

Yes (confidence: extracted)

Yes — symbols where the grammar supports definition tagging; per-language import extraction

Everything else with a recognizable syntax (~250 languages)

lexer token-scan heuristic

Yes, heuristic (confidence: inferred)

No

Shell, YAML, JSON, Markdown

none (inventory-only)

No

No

Notes on coverage:

  • Log-site fidelity: full parsers find log calls from the syntax tree — logger calls (log.Error, logger.warning, console.error, tracing::error!, …), error constructors (fmt.Errorf, errors.New), and any call whose message argument is a string literal of three or more words. Format-string holes (%s, {}, ${…}, f-string expressions) become wildcards in the normalized template.

  • Non-literal messages (a message built entirely at runtime) keep the site — file, line, function, severity — with an empty template and confidence: ambiguous. Still useful: "this function logs at error level."

  • Heuristic languages get log sites only, tagged confidence: inferred, from a lexer token scan (logger-looking callee, open paren, string literal). No symbols, no import edges.

  • Language detection is by file extension first, then well-known basenames (Rakefile, Gemfile), then the shebang line (#!/usr/bin/env python3 and friends). Files with an unknown extension still get the heuristic scan.

  • External dependencies come from manifests, not source: go.mod, package.json, requirements.txt, pyproject.toml, Gemfile, Cargo.toml, pom.xml, build.gradle/build.gradle.kts, and *.csproj.

--no-symbols turns off the symbols/imports/external-deps layer entirely, for teams that want the artifact limited to the file inventory and log sites — see the CLI reference.

Last updated