Skip to main content

Lightweight Azure tenant discovery and visualization via Resource Graph. Enumerates subscriptions and resources, normalizes results, and renders interactive dependency graphs. Supports public and sovereign clouds (Gov, China, Germany, Azure Stack).

Project description

Azure Discovery

Azure Discovery is a lightweight Azure tenant mapper that enumerates subscriptions and resources via Azure Resource Graph, normalizes the results, and renders an interactive dependency graph. The tool exposes both a Typer-based CLI (azure-discovery) and a FastAPI surface so the same discovery workflow can be automated or embedded in other services.

The package is published on PyPI as azure-discovery and can be installed with pip install azure-discovery.

Core capabilities

  • Builds environment-aware credential chains (Azure CLI + DefaultAzureCredential) with guardrails for unsupported clouds.
  • Queries Azure Resource Graph with include/exclude filters, tag constraints, and resource group scopes.
  • Resolves subscriptions automatically when not provided and de-duplicates resources for consistent graph IDs.
  • Produces JSON summaries, console metrics, and PyVis HTML graphs for quick triage.
  • Optionally enumerates Entra ID objects via Microsoft Graph (organization/domains, users, groups, applications, service principals, conditional access policies, risky users) with bounded relationship expansion.
  • Optionally enumerates Azure RBAC role assignments and definitions with principal-to-resource relationship mapping.
  • Optionally enumerates Defender for Cloud security alerts, assessments, and secure scores for security posture analysis.
  • Offers identical request/response contracts (Pydantic models) across CLI and API, following the Receive an Object, Return an Object (RORO) pattern.
  • Supports all Azure clouds: public, Government (GCC/GCC-H), China, Germany, and Azure Stack.
  • Adaptive rate control and intelligent batching for large-scale tenant discovery.
  • API hardening with pluggable authentication (Azure AD, API key), rate limiting, audit logging, and CORS.

Installation

From PyPI (recommended):

pip install azure-discovery

With optional development dependencies:

pip install azure-discovery[dev]

From source (e.g. for development or when embedded in another repo):

git clone https://github.com/maravedi/AzureDiscovery.git
cd AzureDiscovery
pip install -e .[dev]

Package layout

When installed, the package provides the azure_discovery Python package:

azure_discovery/
  __init__.py       # run_discovery, AzureDiscoveryRequest, AzureDiscoveryResponse, etc.
  cli.py            # Typer command surface (entry point: azure-discovery)
  api.py            # FastAPI app for /discover and visualization endpoints
  orchestrator.py   # Async coordinator for enumeration + visualization
  adt_types/        # Pydantic models and custom exceptions
  enumerators/      # Resource Graph query builder and normalization
  reporting/        # Console logging and HTML/PyVis graph generation
  utils/            # Azure SDK clients, graph helpers, structured logging

Usage

CLI

ARM-only discovery:

azure-discovery discover \
  --tenant-id <tenant-guid> \
  --subscription <sub-id-1> --subscription <sub-id-2> \
  --include-type "Microsoft.Compute/virtualMachines" \
  --resource-group core-infra \
  --required-tag environment=prod \
  --visualization-output-dir artifacts/graphs

ARM + Entra discovery (example):

azure-discovery discover \
  --tenant-id <tenant-guid> \
  --subscription <sub-id-1> \
  --include-entra \
  --entra-group-membership-max-groups 50 \
  --entra-group-membership-max-members-per-group 200

ARM + RBAC discovery (example):

azure-discovery discover \
  --tenant-id <tenant-guid> \
  --subscription <sub-id-1> \
  --include-rbac-assignments \
  --include-rbac-definitions \
  --rbac-scope "/subscriptions/<sub-id-1>"

ARM + Defender for Cloud discovery (example):

azure-discovery discover \
  --tenant-id <tenant-guid> \
  --subscription <sub-id-1> \
  --include-defender-cloud \
  --defender-alert-severity High --defender-alert-severity Critical \
  --defender-alert-status Active

Using a config file (JSON/TOML/YAML):

# CLI flags override file values
azure-discovery discover --config examples/config.example.toml

# You can still override specific values
azure-discovery discover --config examples/config.example.toml --include-entra --entra-max-objects 10000

Examples:

Configuration docs:

Tip: you can generate a JSON starter config by running with --preview-request and saving stdout.

Run as a module from source (from the repo root):

python -m azure_discovery.cli discover --help
python -m azure_discovery.cli discover --tenant-id <tenant-guid> --environment azure_gov [options...]

FastAPI

Run the server:

uvicorn azure_discovery.api:app --host 0.0.0.0 --port 8000 --reload

Optional: set AZURE_DISCOVERY_CONFIG=/path/to/discovery.toml to apply default values to incoming requests (request body fields win).

Health check:

curl http://localhost:8000/healthz

Discovery request (example):

curl -X POST http://localhost:8000/discover \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "<tenant-guid>",
    "environment": "azure_public",
    "subscriptions": ["<sub-id>"]
  }'

Enable Entra + relationship expansion (example):

curl -X POST http://localhost:8000/discover \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "<tenant-guid>",
    "environment": "azure_public",
    "subscriptions": ["<sub-id>"],
    "include_entra": true,
    "include_relationships": true,
    "entra_group_membership_max_groups": 50,
    "entra_group_membership_max_members_per_group": 200
  }'

Download visualization:

curl http://localhost:8000/visuals/<file-name> --output graph.html

Python

Async usage:

from azure_discovery import run_discovery
from azure_discovery.adt_types import AzureDiscoveryRequest, AzureEnvironment

request = AzureDiscoveryRequest(
    tenant_id="<tenant-guid>",
    environment=AzureEnvironment.AZURE_PUBLIC,
    subscriptions=["<sub-id>"],
    include_entra=True,
)

response = await run_discovery(request)
print(len(response.nodes), len(response.relationships), response.html_report_path)

Load from config file:

from pathlib import Path

from azure_discovery.utils.config_files import load_request_from_file

request = load_request_from_file(Path("examples/config.example.yaml"))

Sync script wrapper:

import asyncio

from azure_discovery import run_discovery
from azure_discovery.adt_types import AzureDiscoveryRequest, AzureEnvironment

request = AzureDiscoveryRequest(
    tenant_id="<tenant-guid>",
    environment=AzureEnvironment.AZURE_PUBLIC,
)

response = asyncio.run(run_discovery(request))
print(response.total_resources)

Entra ID discovery

When include_entra is enabled, Azure Discovery queries Microsoft Graph and emits normalized nodes using a graph://... ID namespace to avoid collisions with Azure Resource Manager (ARM) IDs.

Entra node types

Typical Entra collections include:

  • Microsoft.Graph/Organization and Microsoft.Graph/Domain
  • Microsoft.Graph/User and Microsoft.Graph/Group
  • Microsoft.Graph/Application and Microsoft.Graph/ServicePrincipal
  • Microsoft.Graph/ConditionalAccessPolicy
  • Microsoft.Graph/RiskyUser

Entra relationships

When include_relationships is enabled, Azure Discovery can emit bounded edges:

  • has_domain (organization -> domain)
  • has_member (group -> member) when group membership expansion is enabled
  • has_owner (application/servicePrincipal -> owner) when ownership expansion is enabled
  • appId (servicePrincipal -> application) correlation edges when both are enumerated

All relationship expansion is capped by request parameters (see CLI options below) to avoid blowing up graphs in large tenants.

Azure RBAC discovery

When include_rbac_assignments or include_rbac_definitions is enabled, Azure Discovery enumerates Azure role-based access control (RBAC) data:

  • Role assignments: Who has what access to which resources
  • Role definitions: Built-in and custom role definitions with their permissions
  • RBAC relationships: Principal → RoleAssignment → Resource graph edges (when include_relationships is enabled)

This capability is useful for security posture assessment, access reviews, and understanding the permission landscape across your Azure estate.

Defender for Cloud discovery

When include_defender_cloud is enabled, Azure Discovery enumerates security findings from Microsoft Defender for Cloud:

  • Security alerts: Active threats, suspicious activity, and security incidents detected across your Azure resources. Each alert includes MITRE ATT&CK tactics and techniques, affected resources, remediation steps, and severity ratings.
  • Security assessments: Vulnerability findings, compliance recommendations, and security best practices. Assessments identify configuration gaps and provide remediation guidance.
  • Secure scores: Subscription-level security posture scores that quantify your current security state (e.g., 42.5/100).

Defender node types

  • Microsoft.Security/alerts: Security alerts with properties like severity (High, Medium, Low, Informational), status (Active, Resolved, Dismissed), MITRE ATT&CK techniques, and affected resources.
  • Microsoft.Security/assessments: Security assessments with severity, status (Healthy, Unhealthy, NotApplicable), categories (Data, Network, Compute, etc.), and remediation descriptions.
  • Microsoft.Security/secureScores: Subscription security posture scores with current/max values and percentage.

Defender relationships

When include_relationships is enabled, Azure Discovery creates edges between security findings and affected resources:

  • affects (alert -> resource): Links security alerts to the VMs, storage accounts, or other resources they impact.
  • affects (assessment -> resource): Links security assessments to the resources that have vulnerabilities or misconfigurations.

These relationships enable security-focused graph queries like "Show me all High severity alerts affecting production VMs" or "Which resources have the most unhealthy assessments?"

Defender filtering

You can filter security findings to reduce noise and focus on critical issues:

# Only High and Critical severity alerts that are Active
azure-discovery discover \
  --tenant-id <tenant-guid> \
  --include-defender-cloud \
  --defender-alert-severity High --defender-alert-severity Critical \
  --defender-alert-status Active

# Only Unhealthy assessments (skip Healthy and NotApplicable)
azure-discovery discover \
  --tenant-id <tenant-guid> \
  --include-defender-cloud \
  --defender-assessment-status Unhealthy

# Alerts only (disable assessments and scores)
azure-discovery discover \
  --tenant-id <tenant-guid> \
  --include-defender-cloud \
  --no-defender-include-assessments \
  --no-defender-include-secure-scores

Config file example (YAML):

include_defender_cloud: true
defender_config:
  include_security_alerts: true
  include_security_assessments: true
  include_secure_scores: true
  alert_severity_filter:
    - High
    - Critical
  alert_status_filter:
    - Active
  assessment_status_filter:
    - Unhealthy

This capability is useful for security operations, vulnerability management, compliance tracking, and prioritizing remediation efforts based on actual threats and exposures.

Prerequisites

  • Python 3.11+
  • Azure CLI 2.60+ (optional, used when --prefer-cli is set) or service principal credentials exported as AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET
  • Azure Resource Graph access (Reader or above on the subscriptions you plan to scan)
  • Microsoft Graph access (only required when using --include-entra)
  • Network egress to management.azure.com and api.azure.com (and to the Graph endpoint for the cloud you select)

Required permissions

Azure Resource Graph (ARM)

Capability Minimum RBAC role Scope recommendation
Run Resource Graph queries Reader, Resource Graph Reader, or any custom role with Microsoft.ResourceGraph/*/read Every subscription you plan to inventory or the parent management group
Auto-discover subscriptions (when --subscription is omitted) Reader on the management group or Directory.Read.All consent for service principals Tenant root (/providers/Microsoft.Management/managementGroups/<root>)
Register Microsoft.ResourceGraph (one-time) Contributor or Owner Each subscription being scanned
Read role assignments Reader or Role Based Access Control Administrator (read-only) Subscription or management group
Read role definitions Reader Subscription
Read Defender for Cloud alerts and assessments Reader or Security Reader Subscription

The tool never mutates resources, but it cannot enumerate subscriptions or call Resource Graph unless the identity has at least Reader at the relevant scope. Grant the narrowest scope that still covers your target estate.

Microsoft Graph (Entra)

Azure Discovery uses Microsoft Graph delegated permissions when running as a signed-in user (for example, Azure CLI/device code flows), and application permissions when running headless (service principal / managed identity).

The following table is a practical starting point for read-only discovery. Always follow least privilege, and prefer narrower resource-specific permissions over broad directory-wide permissions where possible.

Discovery area Typical endpoints Delegated permissions Application permissions Notes
Users /users User.ReadBasic.All or User.Read.All (or Directory.Read.All) User.Read.All (or Directory.Read.All) Guests can't call /users.
Groups + members /groups, /groups/{id}/members Group.Read.All + GroupMember.Read.All (or Directory.Read.All) Group.Read.All + GroupMember.Read.All (or Directory.Read.All) Hidden memberships may require additional permissions depending on tenant settings.
Applications + service principals /applications, /servicePrincipals Application.Read.All (or Directory.Read.All) Application.Read.All (or Directory.Read.All) Needed for enumerating apps/SPs and owner expansion.
Conditional Access policies /identity/conditionalAccess/policies Policy.Read.All Policy.Read.All Delegated access typically requires an Entra role such as Conditional Access Administrator or similar security read roles.
Risky users (Identity Protection) /identityProtection/riskyUsers IdentityRiskyUser.Read.All IdentityRiskyUser.Read.All Requires Entra ID Identity Protection licensing (commonly P2).

References:

Service principal flow (CLI based)

az ad sp create-for-rbac \
  --name azure-discovery-sp \
  --role "Reader" \
  --scopes /subscriptions/<sub-id-1> /subscriptions/<sub-id-2> \
  --years 1

az role assignment create \
  --assignee <appId> \
  --role "Resource Graph Reader" \
  --scope /subscriptions/<sub-id-1>

Export the emitted appId, tenant, and password as AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET. Repeat the role assignment command for every subscription or assign at the management group scope (/providers/Microsoft.Management/managementGroups/<mg-id>) to cover multiple subscriptions at once.

User-assigned permissions (Portal)

  1. Open Azure Portal → Subscriptions → select each target subscription.
  2. Navigate to Access control (IAM)AddAdd role assignment.
  3. Pick the Reader (or Resource Graph Reader) role, then select the user or managed identity that will run AzureDiscovery.
  4. If you want automatic subscription discovery, repeat the assignment at the tenant root management group (visible under Management groups). Users need the Azure RBAC Reader role there.

Provider registration and validation

Run the following once per subscription to ensure the Resource Graph service is registered and the identity can query it:

az account set --subscription <sub-id>
az provider register --namespace Microsoft.ResourceGraph
az graph query -q "Resources | take 1"

Successful output from az graph query confirms both the provider registration and the assigned role. If the command fails with AuthorizationFailed, double-check the scope of the role assignments and replicate them for every subscription you intend to scan.

Configuration reference

Option Description
--config Path to JSON/TOML/YAML config file (AzureDiscoveryRequest shape). CLI flags override file values.
--tenant-id Required Entra ID tenant GUID.
--environment Azure cloud (azure_public, azure_gov, azure_china, azure_germany, azure_stack).
--subscription/-s Repeatable flag to scope runs to explicit subscription IDs. Omit to auto-resolve.
--include-entra Include Entra ID resources via Microsoft Graph.
--include-rbac-assignments Include Azure role assignments in discovery.
--include-rbac-definitions Include Azure role definitions (built-in and custom).
--rbac-scope Filter role assignments by scope (repeatable).
--include-defender-cloud Include Defender for Cloud security findings (alerts, assessments, scores).
--defender-include-alerts/--no-defender-include-alerts Include security alerts from Defender for Cloud (default: true when defender enabled).
--defender-include-assessments/--no-defender-include-assessments Include security assessments (default: true when defender enabled).
--defender-include-secure-scores/--no-defender-include-secure-scores Include secure scores (default: true when defender enabled).
--defender-alert-severity Filter alerts by severity: High, Medium, Low, Informational (repeatable).
--defender-alert-status Filter alerts by status: Active, Resolved, Dismissed (repeatable).
--defender-assessment-severity Filter assessments by severity: High, Medium, Low (repeatable).
--defender-assessment-status Filter assessments by status: Healthy, Unhealthy, NotApplicable (repeatable).
--scale-controls/--no-scale-controls Enable adaptive rate control for large tenants (default: enabled).
--scale-initial-rps Initial requests per second for adaptive rate control (default: 10.0).
--scale-max-concurrent-batches Maximum concurrent batch operations (default: 5).
--scale-initial-batch-size Initial batch size for paginated operations (default: 1000).
--entra-include-organization/--no-entra-include-organization Include organization (tenant root) node.
--entra-include-domains/--no-entra-include-domains Include tenant domains.
--entra-include-users/--no-entra-include-users Include Entra users.
--entra-include-groups/--no-entra-include-groups Include Entra groups.
--entra-include-applications/--no-entra-include-applications Include Entra applications.
--entra-include-conditional-access-policies/--no-entra-include-conditional-access-policies Include conditional access policies (requires permissions).
--entra-include-risky-users/--no-entra-include-risky-users Include risky users (requires permissions).
--entra-group-membership-max-groups Max groups to expand membership for (0 disables expansion).
--entra-group-membership-max-members-per-group Max members per group during expansion (0 disables expansion).
--entra-ownership-max-apps Max applications to expand owners for (0 disables expansion).
--entra-ownership-max-owners-per-app Max owners per app during expansion (0 disables expansion).
--entra-sp-ownership-max-sps Max service principals to expand owners for (0 disables SP ownership expansion).
--entra-sp-ownership-max-owners-per-sp Max owners per service principal during expansion (0 disables expansion).
--include-relationships/--no-include-relationships Include inferred and expanded relationships/edges (Graph expansions require this).
--graph-total-max-objects Maximum total objects across all Graph collections (0 = unlimited).
--entra-max-objects Maximum objects per Entra collection (0 = unlimited).
--include-type / --exclude-type Filter resource types (case-insensitive).
--resource-group Restrict discovery to named resource groups.
--required-tag Enforce tag key=value pairs (repeatable).
--prefer-cli Place Azure CLI credentials at the front of the chain.
--visualization-output-dir Directory for PyVis HTML output (default artifacts/graphs).
--visualization-file Override the generated HTML file name.
--output/-o Write JSON output to file instead of stdout.
--quiet/-q Suppress all logs except errors.
--format/-f Output format: json (default) or json-compact.

Programmatic workflows can instantiate AzureDiscoveryRequest directly and call orchestrator.run_discovery, receiving an AzureDiscoveryResponse that contains resolved subscriptions, normalized nodes, inferred relationships, and an optional html_report_path.

Output and logging separation

By default, the CLI writes JSON results to stdout and logs to stderr. This allows clean piping:

# Pipe JSON output to jq for filtering
azure-discovery discover --tenant-id <id> | jq '.discovered_subscriptions'

# Write output to file and suppress logs
azure-discovery discover --tenant-id <id> --output results.json --quiet

# Compact JSON output for scripting
azure-discovery discover --tenant-id <id> --format json-compact

Development

Quick start

# Install with development dependencies
make install-dev

# Run tests
make test

# Format code
make format

# Run linting
make lint

# Type checking
make typecheck

# Generate coverage report
make coverage

Available make commands

Run make help to see all available commands:

  • make install - Install package dependencies
  • make install-dev - Install with development dependencies
  • make test - Run tests with pytest
  • make lint - Run ruff linter
  • make format - Format code with ruff
  • make typecheck - Run mypy type checking
  • make coverage - Generate test coverage report
  • make clean - Remove build artifacts and cache
  • make run-api - Run FastAPI server locally

Pre-commit hooks

Install pre-commit hooks to automatically run linting and formatting on commit:

pip install pre-commit
pre-commit install

This will run ruff formatting, linting, and mypy type checking before each commit.

Environment variables

Copy .env.example to .env and configure your Azure credentials:

cp .env.example .env
# Edit .env with your credentials

For detailed contributing guidelines, see CONTRIBUTING.md.

Troubleshooting

  • AzureClientError: Unable to enumerate subscriptions – ensure the identity has at least Reader on one subscription and that the Resource Graph service is registered (az provider register --namespace Microsoft.ResourceGraph).
  • AuthorizationFailed / Forbidden (ARM) – confirm the identity has Reader (or Resource Graph Reader) on every subscription (or parent management group) you are scanning, and that your current Azure CLI context is pointing at a subscription you can read (az account show).
  • Resource Graph query failure – check that the tenant/subscription pair belongs to the same cloud you selected, and verify network egress to the relevant resource_manager endpoint (see _ENVIRONMENT_MAP in azure_discovery.utils.azure_clients).
  • 403 Forbidden / Authorization_RequestDenied / Insufficient privileges (Graph) – this usually means required Microsoft Graph permissions were not admin-consented, or (for delegated runs) the signed-in user lacks the Entra admin role required for that dataset (commonly Conditional Access / Identity Protection). If you don't need those datasets, disable them with --no-entra-include-conditional-access-policies and/or --no-entra-include-risky-users.
  • Risky users missing / empty – the Identity Protection APIs require additional permissions and licensing (commonly Entra ID P2); if you don't have that, disable with --no-entra-include-risky-users.
  • Defender for Cloud alerts/assessments empty – verify Defender for Cloud is enabled on the subscription(s) being scanned. The tool gracefully handles subscriptions without Defender enabled (404 errors are logged as warnings).
  • Preflight auth check (CLI) – run azure-discovery discover --tenant-id <tenant-guid> --validate-auth --probe-connectivity to confirm the credential chain can acquire tokens for both ARM and Graph (note: this currently validates token acquisition only).
  • VisualizationError: Failed to render HTML graph – confirm the --visualization-output-dir path exists and is writable; the PyVis writer does not auto-create directories unless it has permissions on each parent.
  • 401 or interaction_required errors – when running non-interactively, use a service principal credential chain and set AZURE_CLIENT_SECRET; the default chain will otherwise attempt to launch an interactive browser flow.
  • Empty graph output – verify filters are not mutually exclusive (e.g., mixing include/exclude for the same type).

Feature status and roadmap

✅ Implemented features

  • Azure RBAC graph – Role assignments and role definitions are fully enumerated and visualized with principal-to-resource relationship mapping.
  • API hardening – FastAPI includes pluggable authentication (Azure AD, API key), rate limiting, audit logging, CORS configuration, and API versioning.
  • Scale controls – Adaptive rate control and intelligent batching automatically handle API throttling and optimize throughput for large tenants.
  • Defender for Cloud – Security alerts, vulnerability assessments, secure scores, and compliance status are integrated with filtering by severity and status.

🔄 Planned features

  • Change tracking – Persistent storage backend with snapshot diffing to track changes over time.
  • Non-Entra SaaS surfaces – M365 and Microsoft Purview enumerators for broader coverage beyond Azure and Entra ID.

For detailed implementation plans, see docs/IMPLEMENTATION_ROADMAP.md.

Publishing to PyPI (maintainers)

To publish a new version to PyPI:

  1. Bump version in pyproject.toml.
  2. Ensure tests pass: pip install -e .[dev] && pytest.
  3. Build: python -m build.
  4. Upload: twine upload dist/azure-discovery-<version>* (requires PyPI credentials or token).

The package uses a single top-level package azure_discovery to avoid namespace conflicts on install. The console script azure-discovery is provided by the [project.scripts] entry in pyproject.toml.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

azure_discovery-0.1.6.tar.gz (112.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

azure_discovery-0.1.6-py3-none-any.whl (87.8 kB view details)

Uploaded Python 3

File details

Details for the file azure_discovery-0.1.6.tar.gz.

File metadata

  • Download URL: azure_discovery-0.1.6.tar.gz
  • Upload date:
  • Size: 112.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for azure_discovery-0.1.6.tar.gz
Algorithm Hash digest
SHA256 e1e2c51c0e263afc68a07e38e2be40ee78ad077cb573d878519500cf1439b5db
MD5 181b414f1e04b94269aa45b51729720b
BLAKE2b-256 6ed5efc62d7446de8ba6fef32efc0a041e5990921b3b912c6a8b67a833179be0

See more details on using hashes here.

File details

Details for the file azure_discovery-0.1.6-py3-none-any.whl.

File metadata

File hashes

Hashes for azure_discovery-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 0893094e02c89f1244bf324f8b4db841406bae0d04f1758b62fad0db5722b657
MD5 7df5aeb1df6aea8bf03aa2822ee13db3
BLAKE2b-256 84a9f267b5ceabe8833270bd56a631cc21c0cadf4ed567b7f22c639bbdb4fa02

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page