Skip to content

🧠 API Reference

CLI

doctor(path='.', verbose=False, debug=False, format='table', output=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' or 'json'")]

Output format: 'table' or 'json'.

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

Optional file path to save JSON result.

None
Source code in src/azure_functions_doctor/cli.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 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
@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' or 'json'")] = "table",
    output: Annotated[Optional[Path], typer.Option(help="Optional path to save JSON result")] = 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' or 'json'.
        output: Optional file path to save JSON result.
    """
    # Validate inputs before proceeding
    _validate_inputs(path, format, output)

    # 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)
    resolved_path = Path(path).resolve()

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

    results = doctor.run_all_checks()

    # 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["status"] == "pass")
    failed_items = sum(1 for section in results for item in section["items"] if item["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)

    # local counts handled below; remove unused placeholders

    # Pre-compute aggregated counts from normalized item['status'] values
    passed_count = 0
    warning_count = 0
    error_count = 0
    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":
                # treat fail as warning (non-fatal) under simplified model
                warning_count += 1
            else:
                warning_count += 1

    if format == "json":
        json_output = results

        if output:
            try:
                output.write_text(json.dumps(json_output, indent=2), encoding="utf-8")
                console.print(f"[green]{format_status_icon('pass')} JSON output saved to:[/green] {output}")
            except (OSError, IOError, PermissionError) as e:
                console.print(f"[red]{format_status_icon('fail')} Failed to write output file:[/red] {e}")
                logger.error(f"Failed to write JSON output to {output}: {e}")
                raise typer.Exit(1) from e
        else:
            print(json.dumps(json_output, indent=2))
        return

    # 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)
    e_label = "error" if error_count == 1 else "errors"
    w_label = "warning" if warning_count == 1 else "warnings"
    p_label = "passed" if passed_count == 1 else "passed"
    console.print(f"  {error_count} {e_label}, {warning_count} {w_label}, {passed_count} {p_label}")
    # Print Exit code line to match the sample and exit with code 1 on errors
    if error_count > 0:
        console.print("Exit code: 1")
        raise typer.Exit(1)
    else:
        console.print("Exit code: 0")

Doctor

Doctor(path='.', allow_v1=False)

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
39
40
41
42
43
44
45
46
47
48
def __init__(self, path: str = ".", allow_v1: bool = False) -> None:
    self.project_path: Path = Path(path).resolve()
    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 rules based on detected programming model.

Source code in src/azure_functions_doctor/doctor.py
85
86
87
88
89
90
91
92
def load_rules(self) -> list[Rule]:
    """Load rules based on detected programming model."""
    if self.programming_model == "v2":
        return self._load_v2_rules()
    elif self.programming_model == "v1":
        return self._load_v1_rules()
    else:
        raise RuntimeError("Unknown programming model; no rules to load")

Handlers

HandlerRegistry()

Registry for diagnostic check handlers with individual handler methods.

Source code in src/azure_functions_doctor/handlers.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
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,
        "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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
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.

Config()

Centralized configuration management with environment variable support.

Source code in src/azure_functions_doctor/config.py
27
28
29
30
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
64
65
66
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
106
107
108
109
110
111
112
113
114
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
78
79
80
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
74
75
76
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
82
83
84
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
94
95
96
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
90
91
92
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
86
87
88
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
 98
 99
100
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
102
103
104
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
68
69
70
71
72
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
116
117
118
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
125
126
127
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
130
131
132
133
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, "?")