Skip to content

🧠 API Reference

CLI

doctor(path='.', verbose=False, debug=False, format='table', output=None, profile=None, rules=None)

Run diagnostics on an Azure Functions application.

Parameters:

Name Type Description Default
path str

Path to the Azure Functions app. Defaults to current directory.

'.'
verbose Annotated[bool, Option(-v, --verbose, help='Show detailed hints for failed checks')]

Show detailed hints for failed checks.

False
debug Annotated[bool, Option(help='Enable debug logging')]

Enable debug logging to stderr.

False
format Annotated[str, Option(help="Output format: 'table', 'json', 'sarif', or 'junit'")]

Output format: 'table', 'json', 'sarif', or 'junit'.

'table'
output Annotated[Optional[Path], Option(help='Optional path to save output result')]

Optional file path to save output result.

None
profile Annotated[Optional[str], Option(help="Rule profile: 'minimal' or 'full'")]

Optional rule profile ('minimal' or 'full').

None
rules Annotated[Optional[Path], Option(help='Optional path to a custom rules.json')]

Optional path to a custom rules.json.

None
Source code in src/azure_functions_doctor/cli.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
@cli.command(name="doctor")
def doctor(
    path: str = ".",
    verbose: Annotated[bool, typer.Option("-v", "--verbose", help="Show detailed hints for failed checks")] = False,
    debug: Annotated[bool, typer.Option(help="Enable debug logging")] = False,
    format: Annotated[str, typer.Option(help="Output format: 'table', 'json', 'sarif', or 'junit'")] = "table",
    output: Annotated[Optional[Path], typer.Option(help="Optional path to save output result")] = None,
    profile: Annotated[Optional[str], typer.Option(help="Rule profile: 'minimal' or 'full'")] = None,
    rules: Annotated[Optional[Path], typer.Option(help="Optional path to a custom rules.json")] = None,
) -> None:
    """
    Run diagnostics on an Azure Functions application.

    Args:
        path: Path to the Azure Functions app. Defaults to current directory.
        verbose: Show detailed hints for failed checks.
        debug: Enable debug logging to stderr.
        format: Output format: 'table', 'json', 'sarif', or 'junit'.
        output: Optional file path to save output result.
        profile: Optional rule profile ('minimal' or 'full').
        rules: Optional path to a custom rules.json.
    """
    # Validate inputs before proceeding
    _validate_inputs(path, format, output)

    if rules is not None and not rules.exists():
        raise typer.BadParameter(f"Rules path does not exist: {rules}")

    # Configure logging based on CLI flags
    if debug:
        setup_logging(level="DEBUG", format_style="structured")
    else:
        # Use environment variable or default to WARNING
        setup_logging(level=None, format_style="simple")

    start_time = time.time()
    # Allow v1 projects when invoked from CLI so we can show warning but continue
    doctor = Doctor(path, allow_v1=True, profile=profile, rules_path=rules)
    resolved_path = Path(path).resolve()

    # Log diagnostic start
    loaded_rules = doctor.load_rules()
    log_diagnostic_start(str(resolved_path), len(loaded_rules))
    results = doctor.run_all_checks(rules=loaded_rules)

    # Calculate execution metrics
    end_time = time.time()
    duration_ms = (end_time - start_time) * 1000

    # Count results for logging
    total_checks = sum(len(section["items"]) for section in results)
    passed_items = sum(1 for section in results for item in section["items"] if item.get("status") == "pass")
    failed_items = sum(1 for section in results for item in section["items"] if item.get("status") == "fail")
    # Note: handlers currently only return "pass"/"fail", not "error"
    errors = 0

    # Log diagnostic completion
    log_diagnostic_complete(total_checks, passed_items, failed_items, errors, duration_ms)

    # Pre-compute aggregated counts from normalized item['status'] values
    passed_count = 0
    warning_count = 0  # explicit 'warn' statuses
    fail_count = 0  # explicit 'fail' statuses
    for section in results:
        for item in section["items"]:
            s = item.get("status")
            if s == "pass":
                passed_count += 1
            elif s == "warn":
                warning_count += 1
            elif s == "fail":
                fail_count += 1
            else:
                warning_count += 1  # unknown treated as warning

    if format == "json":
        generated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
        metadata = {
            "tool_version": __version__,
            "generated_at": generated_at,
            "target_path": str(Path(path).resolve()),
        }
        json_output = {"metadata": metadata, "results": results}
        _write_output(json.dumps(json_output, indent=2), output, "JSON")
        raise typer.Exit(1 if fail_count > 0 else 0)

    if format == "sarif":
        sarif_results = []
        for section in results:
            for item in section["items"]:
                status = item.get("status")
                if status == "pass":
                    continue
                level = "error" if status == "fail" else "warning"
                sarif_results.append(
                    {
                        "ruleId": item.get("label", ""),
                        "message": {"text": item.get("value", "")},
                        "level": level,
                    }
                )

        sarif_output = {
            "version": "2.1.0",
            "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
            "runs": [
                {
                    "tool": {
                        "driver": {
                            "name": "azure-functions-doctor",
                            "version": __version__,
                        }
                    },
                    "results": sarif_results,
                }
            ],
        }
        _write_output(json.dumps(sarif_output, indent=2), output, "SARIF")
        raise typer.Exit(1 if fail_count > 0 else 0)

    if format == "junit":
        import xml.etree.ElementTree as ET

        tests = 0
        failures = 0
        suite = ET.Element(
            "testsuite",
            name="func-doctor",
            tests="0",
            failures="0",
            time=f"{duration_ms / 1000:.3f}",
        )

        for section in results:
            for item in section["items"]:
                tests += 1
                case = ET.SubElement(suite, "testcase", classname=section["title"], name=item.get("label", ""))
                status = item.get("status")
                if status != "pass":
                    failures += 1
                    failure = ET.SubElement(case, "failure", message=item.get("value", ""))
                    failure.text = item.get("hint", "")

        suite.set("tests", str(tests))
        suite.set("failures", str(failures))
        junit_output = ET.tostring(suite, encoding="utf-8", xml_declaration=True).decode("utf-8")
        _write_output(junit_output, output, "JUnit")
        raise typer.Exit(1 if fail_count > 0 else 0)

    # Note: Top header removed per UI change; programming model header intentionally omitted

    if debug:
        console.print("[dim]Debug logging enabled - check stderr for detailed logs[/dim]\n")

    # Table-format user-facing output (requested design)
    console.print("Azure Functions Doctor   ")
    console.print(f"Path: {resolved_path}")

    # Print each section with simple title and items
    for section in results:
        console.print()
        console.print(section["title"])

        for item in section["items"]:
            label = item.get("label", "")
            value = item.get("value", "")
            status = item.get("status", "pass")
            icon = format_status_icon(status)

            # Compose main line: [ICON] Label: value (status)
            line = Text.assemble((f"[{icon}] ", "bold"), (label, "dim"))
            if value:
                line.append(": ")
                line.append(format_detail(status, value))

            # append status in parentheses for clarity on UI when non-pass
            if status != "pass":
                line.append(f" ({status})", "italic dim")

            console.print(line)

            # show hint as 'fix:' only when verbose is enabled
            if status != "pass" and verbose:
                hint = item.get("hint", "")
                if hint:
                    prefix = "↪ "
                    console.print(f"    {prefix}fix: {hint}")

    # Use the precomputed counts from earlier for final output
    console.print()
    # Print Doctor summary at the bottom like the requested sample
    console.print("Doctor summary (to see all details, run azure-functions doctor -v):")
    # Use singular/plural simple form as in sample (error vs errors)
    # Summary now reflects canonical statuses: fails, warnings, passed
    w_label = "warning" if warning_count == 1 else "warnings"
    f_label = "fail" if fail_count == 1 else "fails"
    # 'passed' label remains same for singular/plural in current design
    console.print(f"  {fail_count} {f_label}, {warning_count} {w_label}, {passed_count} passed")
    exit_code = 1 if fail_count > 0 else 0
    console.print(f"Exit code: {exit_code}")
    if exit_code != 0:
        raise typer.Exit(exit_code)

Doctor

Doctor(path='.', allow_v1=False, profile=None, rules_path=None)

Diagnostic runner for Azure Functions apps.

Loads checks from model-specific rule assets located in azure_functions_doctor.assets.rules.v1.json and v2.json. Legacy rules.json support has been removed; callers should ensure the appropriate v1/v2 files are present in package assets.

Source code in src/azure_functions_doctor/doctor.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    path: str = ".",
    allow_v1: bool = False,
    profile: Optional[str] = None,
    rules_path: Optional[Path] = None,
) -> None:
    self.project_path: Path = Path(path).resolve()
    self.profile = profile
    self.rules_path: Optional[Path] = None
    if rules_path is not None:
        resolved = rules_path.resolve()
        if not resolved.is_file():
            raise ValueError(f"rules_path must be an existing file: {resolved}")
        self.rules_path = resolved
    self.programming_model = self._detect_programming_model()
    # If v1 detected in nested function folders (function.json not at project root)
    # and caller did not allow v1, signal incompatibility.
    function_json_files = list(self.project_path.rglob("function.json"))
    nested_v1 = any(f.parent.resolve() != self.project_path for f in function_json_files)

    if nested_v1 and not allow_v1:
        raise SystemExit("v1 programming model detected - limited support")

load_rules()

Load and validate rules based on detected programming model or custom path.

Source code in src/azure_functions_doctor/doctor.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def load_rules(self) -> list[Rule]:
    """Load and validate rules based on detected programming model or custom path."""
    if self.rules_path is not None:
        with self.rules_path.open(encoding="utf-8") as f:
            rules: list[Rule] = json.load(f)
    elif self.programming_model == "v2":
        rules = self._load_v2_rules()
    elif self.programming_model == "v1":
        rules = self._load_v1_rules()
    else:
        raise RuntimeError("Unknown programming model; no rules to load")

    self._validate_rules(rules)
    return sorted(rules, key=lambda r: r.get("check_order", 999))

Handlers

HandlerRegistry()

Registry for diagnostic check handlers with individual handler methods.

Source code in src/azure_functions_doctor/handlers.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def __init__(self) -> None:
    self._handlers = {
        "compare_version": self._handle_compare_version,
        "env_var_exists": self._handle_env_var_exists,
        "path_exists": self._handle_path_exists,
        "file_exists": self._handle_file_exists,
        "package_installed": self._handle_package_installed,
        "package_declared": self._handle_package_declared,
        "source_code_contains": self._handle_source_code_contains,
        "conditional_exists": self._handle_conditional_exists,
        "callable_detection": self._handle_callable_detection,
        "executable_exists": self._handle_executable_exists,
        "any_of_exists": self._handle_any_of_exists,
        "file_glob_check": self._handle_file_glob_check,
        "host_json_property": self._handle_host_json_property,
        "binding_validation": self._handle_binding_validation,
        "cron_validation": self._handle_cron_validation,
    }

handle(rule, path)

Route rule execution to appropriate handler.

Source code in src/azure_functions_doctor/handlers.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def handle(self, rule: Rule, path: Path) -> dict[str, str]:
    """Route rule execution to appropriate handler."""
    check_type = rule.get("type")
    if check_type is None:
        return _create_result("fail", "Missing check type in rule")
    handler = self._handlers.get(check_type)

    if not handler:
        return _create_result("fail", f"Unknown check type: {check_type}")

    try:
        return handler(rule, path)
    except Exception as exc:
        return _handle_specific_exceptions(f"executing {check_type} check", exc)

generic_handler(rule, path)

Execute a diagnostic rule based on its type and condition.

This function maintains backward compatibility while delegating to the registry.

Parameters:

Name Type Description Default
rule Rule

The diagnostic rule to execute.

required
path Path

Path to the Azure Functions project.

required

Returns:

Type Description
dict[str, str]

A dictionary with the status and detail of the check.

Source code in src/azure_functions_doctor/handlers.py
596
597
598
599
600
601
602
603
604
605
606
607
608
609
def generic_handler(rule: Rule, path: Path) -> dict[str, str]:
    """
    Execute a diagnostic rule based on its type and condition.

    This function maintains backward compatibility while delegating to the registry.

    Args:
        rule: The diagnostic rule to execute.
        path: Path to the Azure Functions project.

    Returns:
        A dictionary with the status and detail of the check.
    """
    return _registry.handle(rule, path)

Configuration

Configuration management for Azure Functions Doctor.

Environment variables with FUNC_DOCTOR_ prefix (e.g. FUNC_DOCTOR_LOG_LEVEL) are loaded into Config. These options are reserved for future use; the CLI currently configures logging via logging_config.setup_logging() and does not read from Config. When wiring Config into the CLI/Doctor (e.g. max_file_size_mb, search_timeout_seconds), update this module and the CLI entry point.

Config()

Centralized configuration management with environment variable support.

Options (max_file_size_mb, search_timeout_seconds, etc.) are for future use; not yet wired into the CLI or Doctor. Use get_config() to access the global instance.

Source code in src/azure_functions_doctor/config.py
38
39
40
41
def __init__(self) -> None:
    self._config: Dict[str, Any] = {}
    self._load_defaults()
    self._load_from_environment()

get(key, default=None)

Get configuration value by key.

Source code in src/azure_functions_doctor/config.py
75
76
77
def get(self, key: str, default: Any = None) -> Any:
    """Get configuration value by key."""
    return self._config.get(key, default)

get_custom_rules_path()

Get custom rules file path from environment.

Source code in src/azure_functions_doctor/config.py
117
118
119
120
121
122
123
124
125
def get_custom_rules_path(self) -> Optional[Path]:
    """Get custom rules file path from environment."""
    custom_path = os.getenv("FUNC_DOCTOR_CUSTOM_RULES")
    if custom_path:
        path = Path(custom_path)
        if path.exists():
            return path
        logger.warning(f"Custom rules path does not exist: {custom_path}")
    return None

get_log_format()

Get logging format style.

Source code in src/azure_functions_doctor/config.py
89
90
91
def get_log_format(self) -> str:
    """Get logging format style."""
    return str(self._config["log_format"])

get_log_level()

Get logging level.

Source code in src/azure_functions_doctor/config.py
85
86
87
def get_log_level(self) -> str:
    """Get logging level."""
    return str(self._config["log_level"])

get_max_file_size_mb()

Get maximum file size for processing in MB.

Source code in src/azure_functions_doctor/config.py
93
94
95
def get_max_file_size_mb(self) -> int:
    """Get maximum file size for processing in MB."""
    return int(self._config["max_file_size_mb"])

get_output_width()

Get output width for formatting.

Source code in src/azure_functions_doctor/config.py
105
106
107
def get_output_width(self) -> int:
    """Get output width for formatting."""
    return int(self._config["output_width"])

get_rules_file()

Get rules file name.

Source code in src/azure_functions_doctor/config.py
101
102
103
def get_rules_file(self) -> str:
    """Get rules file name."""
    return str(self._config["rules_file"])

get_search_timeout_seconds()

Get search operation timeout in seconds.

Source code in src/azure_functions_doctor/config.py
97
98
99
def get_search_timeout_seconds(self) -> int:
    """Get search operation timeout in seconds."""
    return int(self._config["search_timeout_seconds"])

is_colors_enabled()

Check if color output is enabled.

Source code in src/azure_functions_doctor/config.py
109
110
111
def is_colors_enabled(self) -> bool:
    """Check if color output is enabled."""
    return bool(self._config["enable_colors"])

is_parallel_execution_enabled()

Check if parallel execution is enabled.

Source code in src/azure_functions_doctor/config.py
113
114
115
def is_parallel_execution_enabled(self) -> bool:
    """Check if parallel execution is enabled."""
    return bool(self._config["parallel_execution"])

set(key, value)

Set configuration value.

Source code in src/azure_functions_doctor/config.py
79
80
81
82
83
def set(self, key: str, value: Any) -> None:
    """Set configuration value."""
    old_value = self._config.get(key)
    self._config[key] = value
    logger.debug(f"Configuration changed: {key}={old_value} -> {value}")

to_dict()

Return configuration as dictionary.

Source code in src/azure_functions_doctor/config.py
127
128
129
def to_dict(self) -> Dict[str, Any]:
    """Return configuration as dictionary."""
    return self._config.copy()

get_config()

Get the global configuration instance.

Source code in src/azure_functions_doctor/config.py
136
137
138
def get_config() -> Config:
    """Get the global configuration instance."""
    return config

override_config(**kwargs)

Override configuration values (useful for testing).

Source code in src/azure_functions_doctor/config.py
141
142
143
144
def override_config(**kwargs: Any) -> None:
    """Override configuration values (useful for testing)."""
    for key, value in kwargs.items():
        config.set(key, value)

Target Resolver

resolve_target_value(target)

Resolve the current value of a target used in version comparison or diagnostics.

Parameters:

Name Type Description Default
target str

The name of the target to resolve. Examples include "python" or "func_core_tools".

required

Returns:

Type Description
str

A string representing the resolved version or value.

Raises:

Type Description
ValueError

If the target is not recognized.

Source code in src/azure_functions_doctor/target_resolver.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def resolve_target_value(target: str) -> str:
    """
    Resolve the current value of a target used in version comparison or diagnostics.

    Args:
        target: The name of the target to resolve. Examples include "python" or "func_core_tools".

    Returns:
        A string representing the resolved version or value.

    Raises:
        ValueError: If the target is not recognized.
    """
    if target == "python":
        return sys.version.split()[0]
    if target == "func_core_tools":
        try:
            output = subprocess.check_output(["func", "--version"], text=True, timeout=10)
            return output.strip()
        except FileNotFoundError:
            logger.debug("Azure Functions Core Tools not found in PATH")
            return "not_installed"
        except subprocess.TimeoutExpired:
            logger.warning("Timeout getting func version")
            return "timeout"
        except TimeoutError:
            logger.warning("Timeout getting func version")
            return "timeout"
        except subprocess.CalledProcessError as e:
            logger.warning(f"func command failed with code {e.returncode}")
            return f"error_{e.returncode}"
        except Exception as exc:
            logger.error(f"Unexpected error getting func version: {exc}")
            return "unknown_error"
    raise ValueError(f"Unknown target: {target}")

Utility

format_detail(status, value)

Return a colored Text element based on status and value.

Parameters:

Name Type Description Default
status str

Diagnostic status ("pass", "fail", "warn").

required
value str

Text to display, typically a description.

required

Returns:

Type Description
Text

A Rich Text object styled with status color.

Source code in src/azure_functions_doctor/utils.py
55
56
57
58
59
60
61
62
63
64
65
66
67
def format_detail(status: str, value: str) -> Text:
    """
    Return a colored Text element based on status and value.

    Args:
        status: Diagnostic status ("pass", "fail", "warn").
        value: Text to display, typically a description.

    Returns:
        A Rich Text object styled with status color.
    """
    color = DETAIL_COLOR_MAP.get(status, "white")
    return Text(value, style=color)

format_result(status)

Return a styled icon Text element based on status.

Parameters:

Name Type Description Default
status str

Diagnostic status ("pass", "fail", "warn").

required

Returns:

Type Description
Text

A Rich Text object with icon and style for headers.

Source code in src/azure_functions_doctor/utils.py
40
41
42
43
44
45
46
47
48
49
50
51
52
def format_result(status: str) -> Text:
    """
    Return a styled icon Text element based on status.

    Args:
        status: Diagnostic status ("pass", "fail", "warn").

    Returns:
        A Rich Text object with icon and style for headers.
    """
    style = STATUS_STYLES.get(status, Style(color="white"))
    icon = format_status_icon(status)
    return Text(icon, style=style)

format_status_icon(status)

Return a simple icon character based on status.

Parameters:

Name Type Description Default
status str

Diagnostic status ("pass", "fail", "warn").

required

Returns: A string icon such as ✓, !, or ✗.

Source code in src/azure_functions_doctor/utils.py
27
28
29
30
31
32
33
34
35
36
37
def format_status_icon(status: str) -> str:
    """
    Return a simple icon character based on status.

    Args:
        status: Diagnostic status ("pass", "fail", "warn").

    Returns:
    A string icon such as ✓, !, or ✗.
    """
    return STATUS_ICONS.get(status, "?")