attocode-code-intel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Middleware for CORS, request logging, rate limiting, and metrics collection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections import defaultdict, deque
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
|
+
from starlette.responses import JSONResponse
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from starlette.requests import Request
|
|
17
|
+
from starlette.responses import Response
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Metrics Collector (module-level singleton)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_MAX_ENTRIES = 10_000
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_metrics_category(path: str) -> str:
|
|
30
|
+
"""Categorize request path into a metrics bucket.
|
|
31
|
+
|
|
32
|
+
Returns one of: "search", "analysis", "graph", "auth", "notify", "health",
|
|
33
|
+
"lsp", "learning", "files", "projects", "embeddings", "other".
|
|
34
|
+
"""
|
|
35
|
+
if path in ("/health", "/ready") or path.startswith("/api/v1/metrics"):
|
|
36
|
+
return "health"
|
|
37
|
+
if path.startswith("/api/v1/auth") or path.startswith("/api/v1/register"):
|
|
38
|
+
return "auth"
|
|
39
|
+
if path.startswith("/api/v1/notify") or path.startswith("/api/v2/notify"):
|
|
40
|
+
return "notify"
|
|
41
|
+
if "/search" in path or "/semantic" in path or "/symbols" in path:
|
|
42
|
+
return "search"
|
|
43
|
+
if "/analysis" in path or "/impact" in path or "/hotspots" in path or "/cross-ref" in path:
|
|
44
|
+
return "analysis"
|
|
45
|
+
if "/graph" in path or "/dependencies" in path or "/deps" in path:
|
|
46
|
+
return "graph"
|
|
47
|
+
if "/lsp" in path:
|
|
48
|
+
return "lsp"
|
|
49
|
+
if "/learning" in path:
|
|
50
|
+
return "learning"
|
|
51
|
+
if "/files" in path:
|
|
52
|
+
return "files"
|
|
53
|
+
if "/projects" in path or "/repos" in path:
|
|
54
|
+
return "projects"
|
|
55
|
+
if "/embeddings" in path:
|
|
56
|
+
return "embeddings"
|
|
57
|
+
return "other"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MetricsCollector:
|
|
61
|
+
"""Thread-safe, in-memory metrics collector using a ring buffer.
|
|
62
|
+
|
|
63
|
+
Tracks request latencies, search statistics, and tool call metrics.
|
|
64
|
+
All data is stored in-memory using ``collections.deque`` with a max
|
|
65
|
+
size of 10,000 entries per category.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, max_entries: int = _MAX_ENTRIES) -> None:
|
|
69
|
+
self._lock = threading.Lock()
|
|
70
|
+
self._max_entries = max_entries
|
|
71
|
+
|
|
72
|
+
# Request metrics: deque of (path_category, method, status, duration_ms, timestamp)
|
|
73
|
+
self._requests: deque[tuple[str, str, int, float, float]] = deque(maxlen=max_entries)
|
|
74
|
+
|
|
75
|
+
# Search metrics: deque of (query, result_count, duration_ms, cache_hit, timestamp)
|
|
76
|
+
self._searches: deque[tuple[str, int, float, bool, float]] = deque(maxlen=max_entries)
|
|
77
|
+
|
|
78
|
+
# Tool call metrics: deque of (tool_name, duration_ms, success, timestamp)
|
|
79
|
+
self._tool_calls: deque[tuple[str, float, bool, float]] = deque(maxlen=max_entries)
|
|
80
|
+
|
|
81
|
+
# -- Recording methods ---------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def record_request(
|
|
84
|
+
self, path: str, method: str, status: int, duration_ms: float
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Record an HTTP request metric."""
|
|
87
|
+
category = _get_metrics_category(path)
|
|
88
|
+
with self._lock:
|
|
89
|
+
self._requests.append((category, method, status, duration_ms, time.time()))
|
|
90
|
+
|
|
91
|
+
def record_search(
|
|
92
|
+
self, query: str, result_count: int, duration_ms: float, cache_hit: bool
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Record a search query metric."""
|
|
95
|
+
with self._lock:
|
|
96
|
+
self._searches.append((query, result_count, duration_ms, cache_hit, time.time()))
|
|
97
|
+
|
|
98
|
+
def record_tool_call(
|
|
99
|
+
self, tool_name: str, duration_ms: float, success: bool
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Record an MCP tool call metric."""
|
|
102
|
+
with self._lock:
|
|
103
|
+
self._tool_calls.append((tool_name, duration_ms, success, time.time()))
|
|
104
|
+
|
|
105
|
+
# -- Percentile computation ----------------------------------------------
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _compute_percentiles(values: list[float]) -> dict[str, float]:
|
|
109
|
+
"""Compute p50, p95, and p99 from a list of values.
|
|
110
|
+
|
|
111
|
+
Returns dict with keys "p50", "p95", "p99". Returns all zeros if
|
|
112
|
+
*values* is empty.
|
|
113
|
+
"""
|
|
114
|
+
if not values:
|
|
115
|
+
return {"p50": 0.0, "p95": 0.0, "p99": 0.0}
|
|
116
|
+
sorted_vals = sorted(values)
|
|
117
|
+
n = len(sorted_vals)
|
|
118
|
+
|
|
119
|
+
def _pct(p: float) -> float:
|
|
120
|
+
idx = int(p / 100.0 * (n - 1))
|
|
121
|
+
return sorted_vals[min(idx, n - 1)]
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"p50": round(_pct(50), 2),
|
|
125
|
+
"p95": round(_pct(95), 2),
|
|
126
|
+
"p99": round(_pct(99), 2),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# -- Aggregation ---------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def get_metrics(self, fmt: str = "json") -> dict[str, Any] | str:
|
|
132
|
+
"""Return aggregated metrics.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
fmt: ``"json"`` returns a dict, ``"prometheus"`` returns
|
|
136
|
+
Prometheus text exposition format.
|
|
137
|
+
"""
|
|
138
|
+
with self._lock:
|
|
139
|
+
requests = list(self._requests)
|
|
140
|
+
searches = list(self._searches)
|
|
141
|
+
tool_calls = list(self._tool_calls)
|
|
142
|
+
|
|
143
|
+
# -- Request metrics -------------------------------------------------
|
|
144
|
+
requests_by_category: dict[str, list[float]] = defaultdict(list)
|
|
145
|
+
status_counts: dict[int, int] = defaultdict(int)
|
|
146
|
+
total_requests = len(requests)
|
|
147
|
+
|
|
148
|
+
for category, _method, status, duration_ms, _ts in requests:
|
|
149
|
+
requests_by_category[category].append(duration_ms)
|
|
150
|
+
status_counts[status] += 1
|
|
151
|
+
|
|
152
|
+
request_latency: dict[str, dict[str, Any]] = {}
|
|
153
|
+
for category, durations in requests_by_category.items():
|
|
154
|
+
request_latency[category] = {
|
|
155
|
+
"count": len(durations),
|
|
156
|
+
"avg_ms": round(sum(durations) / len(durations), 2) if durations else 0.0,
|
|
157
|
+
**self._compute_percentiles(durations),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# -- Search metrics --------------------------------------------------
|
|
161
|
+
search_durations = [d for _, _, d, _, _ in searches]
|
|
162
|
+
search_result_counts = [rc for _, rc, _, _, _ in searches]
|
|
163
|
+
cache_hits = sum(1 for _, _, _, hit, _ in searches if hit)
|
|
164
|
+
cache_misses = len(searches) - cache_hits
|
|
165
|
+
|
|
166
|
+
search_metrics: dict[str, Any] = {
|
|
167
|
+
"total": len(searches),
|
|
168
|
+
"cache_hits": cache_hits,
|
|
169
|
+
"cache_misses": cache_misses,
|
|
170
|
+
"cache_hit_rate": round(cache_hits / len(searches), 4) if searches else 0.0,
|
|
171
|
+
"latency": self._compute_percentiles(search_durations),
|
|
172
|
+
"avg_result_count": (
|
|
173
|
+
round(sum(search_result_counts) / len(search_result_counts), 2)
|
|
174
|
+
if search_result_counts
|
|
175
|
+
else 0.0
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# -- Tool call metrics -----------------------------------------------
|
|
180
|
+
tools_by_name: dict[str, dict[str, Any]] = defaultdict(
|
|
181
|
+
lambda: {"count": 0, "success": 0, "failure": 0, "durations": []}
|
|
182
|
+
)
|
|
183
|
+
for tool_name, duration_ms, success, _ts in tool_calls:
|
|
184
|
+
entry = tools_by_name[tool_name]
|
|
185
|
+
entry["count"] += 1
|
|
186
|
+
if success:
|
|
187
|
+
entry["success"] += 1
|
|
188
|
+
else:
|
|
189
|
+
entry["failure"] += 1
|
|
190
|
+
entry["durations"].append(duration_ms)
|
|
191
|
+
|
|
192
|
+
tool_metrics: dict[str, dict[str, Any]] = {}
|
|
193
|
+
for tool_name, data in tools_by_name.items():
|
|
194
|
+
durations = data["durations"]
|
|
195
|
+
tool_metrics[tool_name] = {
|
|
196
|
+
"count": data["count"],
|
|
197
|
+
"success": data["success"],
|
|
198
|
+
"failure": data["failure"],
|
|
199
|
+
"avg_ms": round(sum(durations) / len(durations), 2) if durations else 0.0,
|
|
200
|
+
**self._compute_percentiles(durations),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
result: dict[str, Any] = {
|
|
204
|
+
"requests": {
|
|
205
|
+
"total": total_requests,
|
|
206
|
+
"by_category": request_latency,
|
|
207
|
+
"by_status": dict(status_counts),
|
|
208
|
+
},
|
|
209
|
+
"search": search_metrics,
|
|
210
|
+
"tools": tool_metrics,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if fmt == "prometheus":
|
|
214
|
+
return self._format_prometheus(result)
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
# -- Prometheus formatting -----------------------------------------------
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _format_prometheus(metrics: dict[str, Any]) -> str:
|
|
221
|
+
"""Format metrics as Prometheus text exposition."""
|
|
222
|
+
lines: list[str] = []
|
|
223
|
+
|
|
224
|
+
# Request metrics
|
|
225
|
+
lines.append("# HELP code_intel_requests_total Total HTTP requests")
|
|
226
|
+
lines.append("# TYPE code_intel_requests_total counter")
|
|
227
|
+
lines.append(f"code_intel_requests_total {metrics['requests']['total']}")
|
|
228
|
+
|
|
229
|
+
for category, data in metrics["requests"]["by_category"].items():
|
|
230
|
+
lines.append(f'code_intel_requests_by_category{{category="{category}"}} {data["count"]}')
|
|
231
|
+
|
|
232
|
+
lines.append("")
|
|
233
|
+
lines.append("# HELP code_intel_request_duration_ms Request latency in milliseconds")
|
|
234
|
+
lines.append("# TYPE code_intel_request_duration_ms summary")
|
|
235
|
+
for category, data in metrics["requests"]["by_category"].items():
|
|
236
|
+
for pct in ("p50", "p95", "p99"):
|
|
237
|
+
quantile = {"p50": "0.5", "p95": "0.95", "p99": "0.99"}[pct]
|
|
238
|
+
lines.append(
|
|
239
|
+
f'code_intel_request_duration_ms{{category="{category}",quantile="{quantile}"}} {data[pct]}'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
for status, count in metrics["requests"]["by_status"].items():
|
|
243
|
+
lines.append(f'code_intel_requests_by_status{{status="{status}"}} {count}')
|
|
244
|
+
|
|
245
|
+
# Search metrics
|
|
246
|
+
lines.append("")
|
|
247
|
+
lines.append("# HELP code_intel_search_total Total search queries")
|
|
248
|
+
lines.append("# TYPE code_intel_search_total counter")
|
|
249
|
+
lines.append(f"code_intel_search_total {metrics['search']['total']}")
|
|
250
|
+
lines.append(f"code_intel_search_cache_hits {metrics['search']['cache_hits']}")
|
|
251
|
+
lines.append(f"code_intel_search_cache_misses {metrics['search']['cache_misses']}")
|
|
252
|
+
lines.append(f"code_intel_search_cache_hit_rate {metrics['search']['cache_hit_rate']}")
|
|
253
|
+
|
|
254
|
+
lines.append("")
|
|
255
|
+
lines.append("# HELP code_intel_search_duration_ms Search latency in milliseconds")
|
|
256
|
+
lines.append("# TYPE code_intel_search_duration_ms summary")
|
|
257
|
+
for pct in ("p50", "p95", "p99"):
|
|
258
|
+
quantile = {"p50": "0.5", "p95": "0.95", "p99": "0.99"}[pct]
|
|
259
|
+
lines.append(
|
|
260
|
+
f'code_intel_search_duration_ms{{quantile="{quantile}"}} {metrics["search"]["latency"][pct]}'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Tool metrics
|
|
264
|
+
lines.append("")
|
|
265
|
+
lines.append("# HELP code_intel_tool_calls_total Total MCP tool calls")
|
|
266
|
+
lines.append("# TYPE code_intel_tool_calls_total counter")
|
|
267
|
+
for tool_name, data in metrics["tools"].items():
|
|
268
|
+
lines.append(f'code_intel_tool_calls_total{{tool="{tool_name}"}} {data["count"]}')
|
|
269
|
+
lines.append(f'code_intel_tool_calls_success{{tool="{tool_name}"}} {data["success"]}')
|
|
270
|
+
lines.append(f'code_intel_tool_calls_failure{{tool="{tool_name}"}} {data["failure"]}')
|
|
271
|
+
for pct in ("p50", "p95", "p99"):
|
|
272
|
+
quantile = {"p50": "0.5", "p95": "0.95", "p99": "0.99"}[pct]
|
|
273
|
+
lines.append(
|
|
274
|
+
f'code_intel_tool_duration_ms{{tool="{tool_name}",quantile="{quantile}"}} {data[pct]}'
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
lines.append("")
|
|
278
|
+
return "\n".join(lines)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Module-level singleton
|
|
282
|
+
metrics_collector = MetricsCollector()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
286
|
+
"""Log request method, path, status, and duration."""
|
|
287
|
+
|
|
288
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
289
|
+
start = time.monotonic()
|
|
290
|
+
response = await call_next(request)
|
|
291
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
292
|
+
logger.info(
|
|
293
|
+
"%s %s → %d (%.1fms)",
|
|
294
|
+
request.method,
|
|
295
|
+
request.url.path,
|
|
296
|
+
response.status_code,
|
|
297
|
+
duration_ms,
|
|
298
|
+
)
|
|
299
|
+
return response
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _get_category(path: str) -> str:
|
|
303
|
+
"""Categorize request path for tiered rate limiting.
|
|
304
|
+
|
|
305
|
+
Returns: "auth" | "notify" | "query"
|
|
306
|
+
"""
|
|
307
|
+
if path.startswith("/api/v1/auth") or path.startswith("/api/v1/register"):
|
|
308
|
+
return "auth"
|
|
309
|
+
if path.startswith("/api/v1/notify"):
|
|
310
|
+
return "notify"
|
|
311
|
+
return "query"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
315
|
+
"""In-memory sliding window rate limiter with tiered limits.
|
|
316
|
+
|
|
317
|
+
Tiers:
|
|
318
|
+
- auth endpoints: 30 RPM (brute-force prevention)
|
|
319
|
+
- notify endpoints: 10x base (initial sync, watch)
|
|
320
|
+
- query endpoints: base RPM (default 300)
|
|
321
|
+
|
|
322
|
+
Keyed by org_id (from auth header) or client IP for unauthenticated requests.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def __init__(self, app, requests_per_minute: int = 300) -> None:
|
|
326
|
+
super().__init__(app)
|
|
327
|
+
# Read from env, fallback to constructor arg
|
|
328
|
+
self.base_rpm = int(os.environ.get("ATTOCODE_RATE_LIMIT_RPM", str(requests_per_minute)))
|
|
329
|
+
self._tier_multipliers = {
|
|
330
|
+
"auth": 0.1, # 30 RPM at base=300
|
|
331
|
+
"notify": 10.0, # 3000 RPM at base=300
|
|
332
|
+
"query": 1.0, # 300 RPM at base=300
|
|
333
|
+
}
|
|
334
|
+
self._windows: dict[str, list[float]] = defaultdict(list)
|
|
335
|
+
|
|
336
|
+
def _get_rpm(self, category: str) -> int:
|
|
337
|
+
mult = self._tier_multipliers.get(category, 1.0)
|
|
338
|
+
return max(1, int(self.base_rpm * mult))
|
|
339
|
+
|
|
340
|
+
def _get_key(self, request: Request, category: str) -> str:
|
|
341
|
+
"""Extract rate limit key from request, scoped by category."""
|
|
342
|
+
# Try to extract org from auth header (best-effort, pre-auth)
|
|
343
|
+
auth = request.headers.get("authorization", "")
|
|
344
|
+
base = f"auth:{hash(auth)}" if auth else f"ip:{request.client.host if request.client else 'unknown'}"
|
|
345
|
+
return f"{category}:{base}"
|
|
346
|
+
|
|
347
|
+
def _check_and_record(self, key: str, now: float, rpm: int | None = None) -> tuple[bool, int, int]:
|
|
348
|
+
"""Check rate limit and record request.
|
|
349
|
+
|
|
350
|
+
Returns (allowed, remaining, reset_seconds).
|
|
351
|
+
"""
|
|
352
|
+
if rpm is None:
|
|
353
|
+
rpm = self.base_rpm
|
|
354
|
+
window = self._windows[key]
|
|
355
|
+
cutoff = now - 60.0
|
|
356
|
+
# Prune expired entries
|
|
357
|
+
self._windows[key] = window = [t for t in window if t > cutoff]
|
|
358
|
+
|
|
359
|
+
remaining = max(0, rpm - len(window))
|
|
360
|
+
reset_seconds = int(cutoff + 60 - now) if window else 60
|
|
361
|
+
|
|
362
|
+
if len(window) >= rpm:
|
|
363
|
+
return False, 0, max(1, reset_seconds)
|
|
364
|
+
|
|
365
|
+
window.append(now)
|
|
366
|
+
return True, remaining - 1, max(1, reset_seconds)
|
|
367
|
+
|
|
368
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
369
|
+
# Skip health endpoints
|
|
370
|
+
if request.url.path in ("/health", "/ready"):
|
|
371
|
+
return await call_next(request)
|
|
372
|
+
|
|
373
|
+
category = _get_category(request.url.path)
|
|
374
|
+
rpm = self._get_rpm(category)
|
|
375
|
+
key = self._get_key(request, category)
|
|
376
|
+
now = time.monotonic()
|
|
377
|
+
allowed, remaining, reset_seconds = self._check_and_record(key, now, rpm)
|
|
378
|
+
|
|
379
|
+
if not allowed:
|
|
380
|
+
return JSONResponse(
|
|
381
|
+
status_code=429,
|
|
382
|
+
content={"detail": "Rate limit exceeded"},
|
|
383
|
+
headers={
|
|
384
|
+
"X-RateLimit-Limit": str(rpm),
|
|
385
|
+
"X-RateLimit-Remaining": "0",
|
|
386
|
+
"X-RateLimit-Reset": str(reset_seconds),
|
|
387
|
+
"Retry-After": str(reset_seconds),
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
response = await call_next(request)
|
|
392
|
+
response.headers["X-RateLimit-Limit"] = str(rpm)
|
|
393
|
+
response.headers["X-RateLimit-Remaining"] = str(remaining)
|
|
394
|
+
response.headers["X-RateLimit-Reset"] = str(reset_seconds)
|
|
395
|
+
return response
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class MetricsMiddleware(BaseHTTPMiddleware):
|
|
399
|
+
"""Record request metrics via the module-level ``metrics_collector``."""
|
|
400
|
+
|
|
401
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
402
|
+
start = time.monotonic()
|
|
403
|
+
response = await call_next(request)
|
|
404
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
405
|
+
metrics_collector.record_request(
|
|
406
|
+
path=request.url.path,
|
|
407
|
+
method=request.method,
|
|
408
|
+
status=response.status_code,
|
|
409
|
+
duration_ms=duration_ms,
|
|
410
|
+
)
|
|
411
|
+
return response
|