Skip to content

Linting & Static Analysis

Module: spanforge.lint
Added in: 1.0.7

spanforge.lint catches instrumentation mistakes at static analysis time — before broken or incomplete events ever reach your export pipeline. It ships as a Python API, a flake8/ruff plugin, and a standalone CLI so it fits wherever your existing quality tools live.


The six error codes

CodeShort descriptionImpact if missed
AO000Syntax error in source fileAll other checks skipped; broken code ships
AO001Event() missing required fieldSilent schema-invalid events
AO002Bare str for identity fieldPII leaks past the redaction pipeline
AO003Unknown event_type string literalEvents silently dropped by consumers
AO004LLM call outside trace spanLLM calls produce no audit telemetry
AO005Emit call outside agent contextOrphaned events with no parent trace

Using run_checks() in tests

The cleanest integration is to call run_checks() directly inside a pytest fixture or test so instrumentation quality gates run alongside your normal test suite:

# tests/test_lint.py
import glob
from spanforge.lint import run_checks

def _all_sources():
    return glob.glob("myapp/**/*.py", recursive=True)

def test_no_lint_errors():
    errors = []
    for path in _all_sources():
        errors.extend(run_checks(open(path).read(), filename=path))

    if errors:
        lines = [f"{e.filename}:{e.line}:{e.col}: {e.code} {e.message}" for e in errors]
        raise AssertionError("spanforge lint errors:\n" + "\n".join(lines))

Using the CLI

# Check a single file
python -m spanforge.lint myapp/pipeline.py

# Check a whole directory tree
python -m spanforge.lint myapp/

# Check the current directory
python -m spanforge.lint .

Sample output:

myapp/pipeline.py:17:1  AO001 Event() is missing required field 'payload'
myapp/pipeline.py:42:12 AO002 actor_id receives a bare str literal; wrap with Redactable()
myapp/pipeline.py:53:5  AO004 LLM provider call outside tracer span context
3 errors in 1 file.

Exit codes:

CodeMeaning
0No errors — clean
1One or more AO-errors found
2Internal error (bad path, etc.)

Using the flake8 plugin

Once spanforge is installed in your environment, AO-codes appear in flake8 and ruff output automatically:

flake8 myapp/
myapp/pipeline.py:17:1: AO001 Event() is missing required field 'payload'
myapp/pipeline.py:42:12: AO002 actor_id receives a bare str literal; wrap with Redactable()

Inline suppression

Suppress a code on a specific line with the standard # noqa comment:

# Suppressing AO002 because actor_id is a system identifier, not user PII:
event = Event(..., actor_id="system-health-monitor")  # noqa: AO002

.flake8 configuration

Add to your .flake8 file to ignore a code project-wide (use sparingly):

[flake8]
extend-ignore = AO003

Adding to CI

GitHub Actions

# .github/workflows/quality.yml
- name: spanforge lint
  run: python -m spanforge.lint myapp/ src/

Make the step a hard failure (it already is — the CLI exits 1 on errors). Combine with flake8 to run both in one step:

- name: Lint
  run: |
    flake8 myapp/
    python -m spanforge.lint myapp/

Makefile

lint:
    ruff check .
    python -m spanforge.lint myapp/

pre-commit hook

# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: spanforge-lint
      name: spanforge instrumentation lint
      language: system
      entry: python -m spanforge.lint
      types: [python]

Fixing each error code

AO000 — Fix the syntax error

# Before (AO000: SyntaxError — unterminated string)
event = Event(event_type="llm.trace.span.completed

All other AO-checks are skipped when AO000 is reported. Fix the syntax error first, then rerun the linter.

AO001 — Add the missing Event() field

# Before  (AO001: payload missing)
event = Event(event_type="llm.trace.span.completed", source="my-app@1.0.0")

# After
event = Event(
    event_type="llm.trace.span.completed",
    source="my-app@1.0.0",
    payload=span.to_dict(),
)

AO002 — Wrap PII fields with Redactable

from spanforge import Redactable

# Before  (AO002)
event = Event(..., actor_id="user-99")

# After
event = Event(..., actor_id=Redactable("user-99", sensitivity="HIGH"))

AO003 — Use the EventType enum

from spanforge.types import EventType

# Before  (AO003: typo in string)
event = Event(event_type="llm.trase.span.completed", ...)

# After
event = Event(event_type=EventType.SPAN_COMPLETED, ...)

AO004 — Wrap LLM calls in a span

from spanforge import tracer

# Before  (AO004)
response = client.chat.completions.create(model="gpt-4o", messages=[...])

# After
async with tracer.span("call-llm"):
    response = client.chat.completions.create(model="gpt-4o", messages=[...])

AO005 — Emit span events inside an agent context

from spanforge._span import agent_run

# Before  (AO005)
emit_span(my_span)

# After
async with agent_run("my-agent") as run:
    emit_span(my_span)

See also