Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
041ba9b
Merge pull request #92 from apidoorman/develop
seniorswe Sep 27, 2025
52d213f
Merge pull request #93 from apidoorman/develop
seniorswe Sep 29, 2025
fc279b3
Merge pull request #94 from apidoorman/develop
seniorswe Sep 29, 2025
a440e73
Merge pull request #95 from apidoorman/develop
seniorswe Sep 30, 2025
aad7328
Merge pull request #97 from apidoorman/develop
seniorswe Oct 4, 2025
31d72a0
Merge pull request #98 from apidoorman/develop
seniorswe Oct 4, 2025
182e061
Merge pull request #99 from apidoorman/develop
seniorswe Oct 4, 2025
a30e48c
Merge pull request #100 from apidoorman/develop
seniorswe Oct 4, 2025
896798b
Merge pull request #101 from apidoorman/develop
seniorswe Oct 5, 2025
e3f8c97
Merge pull request #102 from apidoorman/develop
seniorswe Oct 6, 2025
b78382f
Merge pull request #103 from apidoorman/develop
seniorswe Oct 6, 2025
4768d28
Merge pull request #104 from apidoorman/develop
seniorswe Oct 7, 2025
42a13c4
Merge pull request #105 from apidoorman/develop
seniorswe Oct 15, 2025
bcb8e75
Merge pull request #106 from apidoorman/develop
seniorswe Oct 15, 2025
ff4bb39
Merge pull request #107 from apidoorman/develop
seniorswe Oct 15, 2025
0afd1d6
Merge pull request #108 from apidoorman/develop
seniorswe Oct 15, 2025
6e77dfa
Merge pull request #109 from apidoorman/develop
seniorswe Oct 15, 2025
709fcd5
Merge pull request #110 from apidoorman/develop
seniorswe Oct 15, 2025
b5c82e4
Merge pull request #111 from apidoorman/develop
seniorswe Oct 16, 2025
5e3cf08
Merge pull request #112 from apidoorman/develop
seniorswe Oct 16, 2025
ed67a34
Merge pull request #113 from apidoorman/develop
seniorswe Oct 17, 2025
6110826
Merge pull request #114 from apidoorman/develop
seniorswe Oct 17, 2025
8a9ed77
Merge pull request #115 from apidoorman/develop
seniorswe Oct 17, 2025
88df2af
Merge pull request #116 from apidoorman/develop
seniorswe Oct 17, 2025
12a90cd
Merge pull request #117 from apidoorman/develop
seniorswe Oct 17, 2025
d90833b
Merge pull request #118 from apidoorman/develop
seniorswe Oct 18, 2025
955346d
Merge pull request #119 from apidoorman/develop
seniorswe Oct 19, 2025
e09d852
Merge pull request #120 from apidoorman/develop
seniorswe Oct 19, 2025
d2520a1
Merge pull request #121 from apidoorman/develop
seniorswe Oct 19, 2025
ca1bd2a
Merge pull request #122 from apidoorman/develop
seniorswe Oct 19, 2025
1b352a4
Merge pull request #123 from apidoorman/develop
seniorswe Oct 21, 2025
280b516
Merge pull request #128 from apidoorman/develop
seniorswe Dec 13, 2025
628ab0b
Merge pull request #129 from apidoorman/develop
seniorswe Dec 13, 2025
5691975
Merge pull request #130 from apidoorman/develop
seniorswe Dec 13, 2025
d08cc3d
Merge pull request #131 from apidoorman/develop
seniorswe Dec 13, 2025
4de82f3
Merge pull request #132 from apidoorman/develop
seniorswe Dec 19, 2025
e4f95ee
Merge pull request #133 from apidoorman/develop
seniorswe Dec 19, 2025
96c60c0
Merge pull request #135 from apidoorman/develop
seniorswe Jan 3, 2026
79f4aea
Merge pull request #136 from apidoorman/develop
seniorswe Jan 19, 2026
63426f5
Merge pull request #137 from apidoorman/develop
seniorswe Jan 25, 2026
0d201cd
Merge pull request #138 from apidoorman/develop
seniorswe Jan 25, 2026
8e724b2
Update README.md
seniorswe Jan 25, 2026
020f58d
Delete fix.md
seniorswe Jan 25, 2026
a329e83
Merge pull request #139 from apidoorman/develop
seniorswe Jan 26, 2026
885e45a
test fix
Jan 26, 2026
45216ca
Merge pull request #140 from apidoorman/bug_csfr-fix
seniorswe Jan 26, 2026
a599449
bug fixes
Jan 27, 2026
634bd89
Bug fixes, log updates
Jan 27, 2026
4dab2e5
bug and log fix
Jan 27, 2026
de8fae6
Merge pull request #147 from apidoorman/bug-fixes-1
seniorswe Jan 27, 2026
0c8791c
Sec flaw on user permissions fix
Jan 28, 2026
36ee885
Merge pull request #149 from apidoorman/bug-fixes-2
seniorswe Jan 28, 2026
386439c
fix conflicts
Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Run a local demo instance in seconds.

```bash
# Clone and launch instantly
cp .env.example .env
docker compose -f docker-compose.yml -f docker-compose.demo.yml up --build
```

Expand Down
62 changes: 44 additions & 18 deletions backend-services/doorman.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
from routes.grpc_routes import grpc_router
from utils.audit_util import audit
from middleware.security_audit_middleware import SecurityAuditMiddleware
from middleware.logging_middleware import GlobalLoggingMiddleware
from middleware.latency_injection_middleware import LatencyInjectionMiddleware
from middleware.websocket_reject_middleware import WebSocketRejectMiddleware
from utils.auth_blacklist import purge_expired_tokens
Expand All @@ -111,6 +112,7 @@
restore_memory_from_file,
)
from utils.metrics_util import metrics_store
from utils.enhanced_metrics_util import enhanced_metrics_store
from utils.response_util import process_response
from utils.security_settings_util import (
get_cached_settings,
Expand Down Expand Up @@ -368,11 +370,18 @@ async def app_lifespan(app: FastAPI):
except Exception as e:
gateway_logger.debug(f'Metrics restore skipped: {e}')

ENHANCED_METRICS_FILE = os.path.join(LOGS_DIR, 'enhanced_metrics.json')
try:
enhanced_metrics_store.load_from_file(ENHANCED_METRICS_FILE)
except Exception as e:
gateway_logger.debug(f'Enhanced metrics restore skipped: {e}')

async def _metrics_autosave(interval_s: int = 60):
while True:
try:
await asyncio.sleep(interval_s)
metrics_store.save_to_file(METRICS_FILE)
enhanced_metrics_store.save_to_file(ENHANCED_METRICS_FILE)
except asyncio.CancelledError:
break
except Exception:
Expand Down Expand Up @@ -647,6 +656,8 @@ async def _sighup_reload():
try:
METRICS_FILE = os.path.join(LOGS_DIR, 'metrics.json')
metrics_store.save_to_file(METRICS_FILE)
ENHANCED_METRICS_FILE = os.path.join(LOGS_DIR, 'enhanced_metrics.json')
enhanced_metrics_store.save_to_file(ENHANCED_METRICS_FILE)
except Exception:
pass

Expand Down Expand Up @@ -1484,6 +1495,7 @@ async def send_wrapper(message):


doorman.add_middleware(PlatformCORSMiddleware)
doorman.add_middleware(GlobalLoggingMiddleware)

# Add tier-based rate limiting middleware (skip in live/test to avoid 429 floods)
try:
Expand Down Expand Up @@ -1514,41 +1526,45 @@ async def send_wrapper(message):
@doorman.middleware('http')
async def request_id_middleware(request: Request, call_next):
try:
from utils.correlation_util import get_correlation_id, set_correlation_id

rid = (
request.headers.get('x-request-id')
getattr(request.state, 'request_id', None)
or get_correlation_id()
or request.headers.get('x-request-id')
or request.headers.get('request-id')
or request.headers.get('x-request-id'.title())
or request.headers.get('X-Request-ID')
)
if not rid:
rid = str(uuid.uuid4())

try:
# Store in state
if not hasattr(request.state, 'request_id'):
request.state.request_id = rid
except Exception:
pass

try:
from utils.correlation_util import set_correlation_id
# Ensure correlation ID is set
set_correlation_id(rid)

set_correlation_id(rid)
except Exception:
pass
# Optional logging
try:
settings = get_cached_settings()
trust_xff = bool(settings.get('trust_x_forwarded_for'))
trust_xff = bool(settings.get('trust_x_forwarded_for', False))
direct_ip = getattr(getattr(request, 'client', None), 'host', None)
effective_ip = _policy_get_client_ip(request, trust_xff)
gateway_logger.info(
f'{rid} | Entry: client_ip={direct_ip} effective_ip={effective_ip} method={request.method} path={str(request.url.path)}'
f'Entry: client_ip={direct_ip} effective_ip={effective_ip} method={request.method} path={str(request.url.path)}'
)
except Exception:
pass

response = await call_next(request)

try:
response.headers['X-Request-ID'] = rid
response.headers['request_id'] = rid
except Exception as e:
gateway_logger.warning(f'Failed to set response headers: {str(e)}')

return response
except Exception as e:
gateway_logger.error(f'Request ID middleware error: {str(e)}', exc_info=True)
Expand Down Expand Up @@ -1629,10 +1645,12 @@ async def security_headers(request: Request, call_next):
# Build formatters
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
request_id = getattr(record, 'request_id', 'no-request-id')
payload = {
'time': self.formatTime(record, '%Y-%m-%dT%H:%M:%S'),
'name': record.name,
'level': record.levelname,
'request_id': request_id,
'message': record.getMessage(),
}
try:
Expand All @@ -1654,7 +1672,7 @@ def format(self, record: logging.LogRecord) -> str:
_file_handler.setFormatter(
JSONFormatter()
if _fmt_is_json
else logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
else logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(request_id)s | %(message)s')
)
except Exception as _e:
# Attempt fallback to /tmp which is commonly writable on containers
Expand All @@ -1670,7 +1688,7 @@ def format(self, record: logging.LogRecord) -> str:
_file_handler.setFormatter(
JSONFormatter()
if _fmt_is_json
else logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
else logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(request_id)s | %(message)s')
)
logging.getLogger('doorman.gateway').warning(
f'Primary LOGS_DIR={LOGS_DIR} not writable ({_e}); falling back to {_tmp_dir}'
Expand All @@ -1685,12 +1703,16 @@ def format(self, record: logging.LogRecord) -> str:
# Configure all doorman loggers to use the same handler and prevent propagation
def configure_logger(logger_name: str) -> logging.Logger:
logger = logging.getLogger(logger_name)
logger.setLevel(logging.INFO)
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
numeric_level = getattr(logging, log_level, logging.INFO)
logger.setLevel(numeric_level)
logger.propagate = False

for handler in logger.handlers[:]:
logger.removeHandler(handler)

from utils.correlation_util import correlation_id, RequestIdFilter

class RedactFilter(logging.Filter):
"""Comprehensive logging redaction filter for sensitive data.

Expand Down Expand Up @@ -1767,19 +1789,21 @@ def filter(self, record: logging.LogRecord) -> bool:
return True

console = logging.StreamHandler(stream=sys.stdout)
console.setLevel(logging.INFO)
console.setLevel(numeric_level)
console.setFormatter(
JSONFormatter()
if _fmt_is_json
else logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
else logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(request_id)s | %(message)s')
)
console.addFilter(RequestIdFilter())
console.addFilter(RedactFilter())
logger.addHandler(console)

if _file_handler is not None:
if not any(
isinstance(f, logging.Filter) and hasattr(f, 'PATTERNS') for f in _file_handler.filters
):
_file_handler.addFilter(RequestIdFilter())
_file_handler.addFilter(RedactFilter())
logger.addHandler(_file_handler)
return logger
Expand All @@ -1795,7 +1819,9 @@ def filter(self, record: logging.LogRecord) -> bool:

_mem_enabled = os.getenv('MEMORY_LOG_ENABLED', 'true').lower() != 'false'
if _mem_enabled:
_mem_handler = MemoryLogHandler()
_lvl = os.getenv('LOG_LEVEL', 'INFO').upper()
_num_lvl = getattr(logging, _lvl, logging.INFO)
_mem_handler = MemoryLogHandler(level=_num_lvl)
for _name in (
'doorman.gateway',
'doorman.logging',
Expand Down
47 changes: 39 additions & 8 deletions backend-services/middleware/analytics_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,27 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:
method = request.method
path = str(request.url.path)

# Estimate request size (headers + body)
# Estimate request size (headers + body + protocol overhead)
request_size = 0
try:
# Headers size
request_size += sum(len(k) + len(v) for k, v in request.headers.items())
# Body size (if available)
# Request line: "METHOD /path?query HTTP/1.1\r\n"
request_line = f"{request.method} {request.url.path}"
if request.url.query:
request_line += f"?{request.url.query}"
request_line += " HTTP/1.1\r\n"
request_size += len(request_line)

# Headers with proper separators (key: value\r\n)
for k, v in request.headers.items():
request_size += len(f"{k}: {v}\r\n")
request_size += 2 # Final \r\n separator

# Body size
if 'content-length' in request.headers:
request_size += int(request.headers['content-length'])
elif hasattr(request, '_body') and request._body:
# Fallback: use actual body if available
request_size += len(request._body)
except Exception:
pass

Expand Down Expand Up @@ -83,14 +96,29 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Extract response metadata
status_code = response.status_code

# Estimate response size
# Estimate response size (headers + body + protocol overhead)
response_size = 0
try:
# Headers size
response_size += sum(len(k) + len(v) for k, v in response.headers.items())
# Body size (if available)
# Status line: "HTTP/1.1 200 OK\r\n"
status_text = response.status_code
try:
from http import HTTPStatus
status_text = f"{response.status_code} {HTTPStatus(response.status_code).phrase}"
except Exception:
status_text = str(response.status_code)
response_size += len(f"HTTP/1.1 {status_text}\r\n")

# Headers with proper separators (key: value\r\n)
for k, v in response.headers.items():
response_size += len(f"{k}: {v}\r\n")
response_size += 2 # Final \r\n separator

# Body size
if 'content-length' in response.headers:
response_size += int(response.headers['content-length'])
# Note: For streaming/chunked responses without Content-Length,
# we can't accurately measure body size in middleware without wrapping.
# This is acceptable as most responses have Content-Length.
except Exception:
pass

Expand All @@ -104,6 +132,9 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response:
except Exception:
pass

# Extract request_id for logging
request_id = getattr(request.state, 'request_id', None) or request.headers.get('X-Request-ID') or 'unknown'

# Parse API and endpoint from path
api_key, endpoint_uri = self._parse_api_endpoint(path)

Expand Down
68 changes: 68 additions & 0 deletions backend-services/middleware/logging_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import logging
import time
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request

logger = logging.getLogger('doorman.gateway')

class GlobalLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 1. Generate or extract Request ID
from utils.correlation_util import correlation_id

request_id = (
getattr(request.state, 'request_id', None)
or correlation_id.get()
or request.headers.get('X-Request-ID')
or request.headers.get('request-id')
)
if not request_id:
request_id = str(uuid.uuid4())

correlation_id.set(request_id)

# Store in state if not already present
if not hasattr(request.state, 'request_id'):
request.state.request_id = request_id

# 2. Start Timer
start_time = time.time()

# 3. Log Request Entry (Optional - helpful for incomplete requests)
# We can default to 'platform' or 'gateway' in type inference if needed.
# Using a consistent prefix helps.
# logger.info(f"Request started: {request.method} {request.url.path}")

try:
# 4. Process Request
response = await call_next(request)

# 5. Calculate Duration
duration = (time.time() - start_time) * 1000

# 6. Log Response
# Format matches LoggingService extraction regexes:
# Endpoint: {method} {path}
# status_code: {code}
# Total time: {ms}ms
logger.info(
f"Endpoint: {request.method} {request.url.path} "
f"| status_code: {response.status_code} "
f"| Total time: {duration:.2f}ms"
)

# Ensure Request ID is returned in headers for debugging
response.headers['X-Request-ID'] = request_id

return response

except Exception as e:
# 7. Log Unhandled Exceptions
duration = (time.time() - start_time) * 1000
logger.error(
f"Request failed: {request.method} {request.url.path} "
f"| Error: {str(e)} | Time: {duration:.2f}ms",
exc_info=True
)
raise
Loading
Loading