Introduction
The AI Tender Opportunity Scanner has now reached an advanced stage of development.
The platform can:
- Collect tender opportunities from external sources
- Normalize and store tender data
- Analyze tender documents with AI
- Match opportunities to organization profiles
- Rank tenders by relevance
- Manage saved opportunities
- Track bid pipelines
- Generate analytics
- Send email notifications
- Deliver Slack and Microsoft Teams alerts
- Authenticate users
- Manage organizations and permissions
- Isolate tenant data
- Process subscriptions and billing
- Track AI usage and infrastructure costs
- Route AI requests across models and providers
These capabilities create a useful and commercially viable SaaS platform.
However, a production system cannot be operated safely by simply deploying the application and waiting for users to report problems.
We need to know:
Is the API responding?Are background workers running?Are tender ingestion jobs failing?Are AI providers timing out?Are webhook events being processed?Are emails being delivered?Are database queries becoming slower?Is one organization generating abnormal traffic?Is the error rate increasing?Are subscription events delayed?Is the platform approaching capacity?
Traditional application monitoring often focuses on whether servers are running.
Observability goes further.
It helps us understand the internal state of the entire system by examining its outputs:
- Logs
- Metrics
- Traces
- Events
- Errors
- Alerts
In this article, we will build the operational foundation required to run the Tender Opportunity Scanner reliably in production.
We will design:
- Structured application logging
- Request correlation IDs
- Centralized log collection
- Application metrics
- Infrastructure metrics
- Distributed tracing
- Error tracking
- Health and readiness endpoints
- Background-job monitoring
- AI-provider monitoring
- Billing webhook monitoring
- Alert policies
- Incident severity levels
- Operational dashboards
- Service-level indicators
- Service-level objectives
- Audit-safe logging
- Data-retention policies
- Testing and incident response workflows
The goal is not merely to collect more data.
The goal is to make production behavior understandable and actionable.
Development Progress
Project Scope and Roadmap ████████████████████ 100%MVP Definition ████████████████████ 100%Technology Stack ████████████████████ 100%System Architecture ████████████████████ 100%Database Design ████████████████████ 100%Backend Project Structure ████████████████████ 100%SQLAlchemy Models ████████████████████ 100%Alembic Migration ████████████████████ 100%Repository Layer ████████████████████ 100%CRUD Service Layer ████████████████████ 100%Tender Ingestion Pipeline ████████████████████ 100%AI Analysis Pipeline ████████████████████ 100%AI Matching Engine ████████████████████ 100%React Dashboard ████████████████████ 100%Tender Detail Page ████████████████████ 100%Saved Opportunities Workspace ████████████████████ 100%Pipeline Analytics Dashboard ████████████████████ 100%Notifications & Monitoring ████████████████████ 100%Email Notifications ████████████████████ 100%Slack & Teams Integrations ████████████████████ 100%Authentication & Organizations ████████████████████ 100%Multi-Tenant Data Isolation ████████████████████ 100%Billing & Subscription System ████████████████████ 100%AI Usage & Cost Optimization ████████████████████ 100%Production Observability ████████████████████ 100%Deployment & Release Automation ░░░░░░░░░░░░░░░░░░░░ 0%
What Is Observability?
Observability is the ability to understand what is happening inside a system by examining the information it produces.
A production platform emits several forms of operational data.
Application | +---- Logs +---- Metrics +---- Traces +---- Errors +---- Events
Each signal answers different questions.
Logs
Logs record individual events.
Examples:
User logged inTender ingestion startedAI analysis completedWebhook signature rejectedEmail delivery failedSubscription updated
Logs are useful for investigating specific events and failures.
Metrics
Metrics record numerical values over time.
Examples:
API requests per secondError rateAverage response timeQueue depthActive usersAI request costDatabase connection usage
Metrics are useful for dashboards, trends, and alerts.
Traces
Traces follow a request or job across multiple services.
Example:
Upload Tender | vFastAPI Endpoint | vStorage Service | vBackground Queue | vDocument Parser | vAI Provider | vDatabase
A trace helps identify which component caused a slow or failed workflow.
Errors
Error-tracking systems group exceptions and provide:
- Stack traces
- Frequency
- Affected users
- Environment
- Release version
- Request context
- First and latest occurrence
This is more useful than searching raw log files for exception messages.
Observability Architecture
FastAPI API | +---- Structured Logs +---- Metrics +---- Traces +---- Errors | vObservability Collector | +---- Log Platform +---- Metrics Platform +---- Trace Platform +---- Error Tracker | vDashboards and Alerts | vOperations Team
The platform may use separate tools or a combined observability service.
Core Observability Principles
The implementation should follow several principles.
Every request receives a correlation IDEvery background job receives a job IDEvery log is structuredEvery critical workflow emits metricsEvery external dependency is monitoredEvery alert has an ownerEvery alert should require actionSensitive customer data is excluded
Observability data must help operators solve problems rather than create noise.
Monitoring Versus Observability
Monitoring tells us that a known problem has occurred.
Example:
API error rate exceeds 5%
Observability helps us investigate unfamiliar problems.
Example:
Why are Professional-plan customers experiencing slow tender analysis only when PDF files exceed 100 pages?
Both capabilities are required.
Recommended Project Structure
Create an observability module.
app/├── observability/│ ├── __init__.py│ ├── logging.py│ ├── context.py│ ├── metrics.py│ ├── tracing.py│ ├── errors.py│ ├── middleware.py│ ├── health.py│ └── redaction.py│├── api/│ └── routes/│ └── health.py│└── services/ └── operational_event_service.py
This centralizes operational concerns and prevents inconsistent logging across services.
Structured Logging
Plain-text logs are difficult to query reliably.
Bad:
Tender 551 failed for Northstar because provider timed out
Better:
{ "event": "tender_analysis_failed", "tender_id": 551, "organization_id": 10, "provider": "primary_ai", "error_type": "timeout", "correlation_id": "req_7e3b...", "severity": "error"}
Structured logs let the log platform index individual fields.
Logging Configuration
Create:
app/observability/logging.py
The configuration should define:
- JSON formatting
- Log level
- Timestamp format
- Application name
- Environment
- Release version
- Correlation context
- Redaction filters
Example:
import loggingimport sysfrom pythonjsonlogger import jsonloggerdef configure_logging( *, service_name: str, environment: str, release_version: str, log_level: str = "INFO",) -> None: handler = logging.StreamHandler(sys.stdout) formatter = jsonlogger.JsonFormatter( "%(asctime)s %(levelname)s %(name)s " "%(message)s %(correlation_id)s " "%(organization_id)s %(user_id)s" ) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.addHandler(handler) root_logger.setLevel(log_level.upper()) logging.getLogger(__name__).info( "logging_configured", extra={ "service_name": service_name, "environment": environment, "release_version": release_version, }, )
In a production implementation, a custom formatter can inject standard fields automatically.
Standard Log Fields
Every structured log should use consistent field names.
timestampseverityservice_nameenvironmentrelease_versioneventmessagecorrelation_idtrace_idspan_idorganization_iduser_idrequest_methodrequest_pathstatus_codeduration_ms
Workflow-specific fields can then be added.
Example:
tender_idjob_idprovidermodelsubscription_idwebhook_event_id
Consistency is more important than logging every possible value.
Event Names
Use stable machine-readable event names.
Good:
tender_ingestion_startedtender_ingestion_completedtender_ingestion_failedai_analysis_startedai_analysis_completedai_analysis_failed
Avoid vague messages such as:
Something went wrong
Stable event names improve queries, dashboards, and alert rules.
Correlation IDs
A correlation ID connects all logs generated by one request or workflow.
Example:
Browser Request | vcorrelation_id = req_abc123 | +---- API log +---- Database log +---- Queue message +---- Worker log +---- AI-provider log
Without a shared identifier, investigating a distributed workflow becomes much harder.
Correlation Context
Create:
app/observability/context.py
Use contextvars so request information remains available throughout asynchronous execution.
from contextvars import ContextVarcorrelation_id_context: ContextVar[ str | None] = ContextVar( "correlation_id", default=None,)organization_id_context: ContextVar[ int | None] = ContextVar( "organization_id", default=None,)user_id_context: ContextVar[ int | None] = ContextVar( "user_id", default=None,)
Correlation Middleware
Create:
app/observability/middleware.py
from uuid import uuid4from fastapi import Requestfrom starlette.middleware.base import ( BaseHTTPMiddleware,)class CorrelationIdMiddleware( BaseHTTPMiddleware): async def dispatch( self, request: Request, call_next, ): incoming_id = request.headers.get( "X-Correlation-ID" ) correlation_id = ( incoming_id if incoming_id else f"req_{uuid4().hex}" ) token = correlation_id_context.set( correlation_id ) try: response = await call_next(request) response.headers[ "X-Correlation-ID" ] = correlation_id return response finally: correlation_id_context.reset(token)
The response returns the identifier so support teams can ask customers for it.
Validating Incoming Correlation IDs
Do not trust arbitrary, excessively long values from external clients.
Validate:
Maximum lengthAllowed charactersExpected prefixNo line breaksNo control characters
Invalid values should be replaced with a server-generated identifier.
Request Logging Middleware
Create middleware that records:
- HTTP method
- Path
- Status code
- Duration
- Correlation ID
- User and organization when available
import timeimport loggingfrom fastapi import Requestfrom starlette.middleware.base import ( BaseHTTPMiddleware,)logger = logging.getLogger(__name__)class RequestLoggingMiddleware( BaseHTTPMiddleware): async def dispatch( self, request: Request, call_next, ): started_at = time.perf_counter() try: response = await call_next(request) except Exception: duration_ms = ( time.perf_counter() - started_at ) * 1000 logger.exception( "http_request_failed", extra={ "request_method": request.method, "request_path": request.url.path, "duration_ms": round(duration_ms, 2), }, ) raise duration_ms = ( time.perf_counter() - started_at ) * 1000 logger.info( "http_request_completed", extra={ "request_method": request.method, "request_path": request.url.path, "status_code": response.status_code, "duration_ms": round(duration_ms, 2), }, ) return response
Do not log query parameters or request bodies by default because they may contain sensitive information.
Logging Authenticated Context
After authentication succeeds, add:
organization_iduser_idmembership_id
to the current logging context.
Do not place:
- Access tokens
- Refresh tokens
- Passwords
- Invitation tokens
- Raw authorization headers
in logs.
Sensitive Data Redaction
Create:
app/observability/redaction.py
Sensitive keys may include:
passwordaccess_tokenrefresh_tokenauthorizationapi_keysecretwebhook_secretpayment_methodtax_id
Example:
SENSITIVE_KEYS = { "password", "access_token", "refresh_token", "authorization", "api_key", "secret", "webhook_secret",}def redact_mapping( value: dict,) -> dict: redacted = {} for key, item in value.items(): normalized_key = key.lower() if normalized_key in SENSITIVE_KEYS: redacted[key] = "[REDACTED]" elif isinstance(item, dict): redacted[key] = redact_mapping(item) else: redacted[key] = item return redacted
Redaction should happen before data reaches the log formatter.
Avoid Logging Tender Content
Tender documents may contain:
- Confidential commercial information
- Personal data
- Supplier details
- Pricing data
- Internal notes
- Uploaded attachments
Log identifiers and metadata instead.
Bad:
Prompt:Analyze this confidential 180-page tender...
Better:
{ "event": "ai_analysis_started", "organization_id": 10, "tender_id": 551, "document_hash": "sha256:...", "document_pages": 180, "prompt_version": "risk-analysis-v3"}
Log Levels
Use log levels consistently.
DEBUGDetailed development diagnosticsINFONormal business and operational eventsWARNINGUnexpected condition that did not stop processingERROROperation failedCRITICALMajor platform or security failure
Do not use ERROR for ordinary validation failures.
Example:
User entered an invalid email:INFO or no logDatabase unavailable:ERRORAll database replicas unavailable:CRITICAL
Development and Production Logging
Development logs can be more readable.
Production logs should be machine-readable.
Development:Colored human-readable outputProduction:JSON to standard output
Container platforms can collect standard output centrally.
Avoid writing logs only to local container files because containers may be replaced at any time.
Centralized Log Collection
A centralized platform allows operators to query logs from:
- API instances
- Background workers
- Schedulers
- Ingestion services
- AI processing workers
- Webhook processors
Architecture:
API ContainersWorker ContainersSchedulerWebhook Service | vLog Collector | vCentral Log Storage | vSearch and Dashboards
Possible technology categories include:
Managed cloud loggingOpenSearch or ElasticsearchGrafana LokiObservability SaaS platform
The architecture should not depend heavily on a single vendor.
Log Retention
Not all logs need permanent storage.
Example policy:
Debug logs:7 daysApplication logs:30–90 daysSecurity logs:12 monthsAudit events:According to contractual and regulatory requirements
Longer retention increases cost and privacy exposure.
Retention should match actual operational and compliance needs.
Metrics Architecture
Application | vMetrics Endpoint or Collector | vTime-Series Database | vDashboards and Alerts
Metrics should have:
- Stable names
- Clear units
- Low-cardinality labels
- Documented meanings
The Cardinality Problem
Metric labels create separate time series.
Bad label:
tender_id
A unique tender ID may create millions of series.
Good labels:
environmentserviceendpoint_groupstatus_classprovidermodel_familyjob_type
High-cardinality identifiers belong in logs and traces, not metric labels.
Core API Metrics
Track:
HTTP request countHTTP error countRequest durationActive requestsRequest body sizeResponse body sizeRate-limit events
Example metric names:
http_requests_totalhttp_request_duration_secondshttp_requests_in_progresshttp_response_errors_total
Metrics Module
Create:
app/observability/metrics.py
Example:
from prometheus_client import ( Counter, Histogram,)HTTP_REQUESTS_TOTAL = Counter( "http_requests_total", "Total HTTP requests", [ "method", "route", "status_class", ],)HTTP_REQUEST_DURATION = Histogram( "http_request_duration_seconds", "HTTP request duration", [ "method", "route", ],)
Use route templates such as:
/saved-opportunities/{opportunity_id}
rather than actual paths containing unique identifiers.
Metrics Middleware
class MetricsMiddleware( BaseHTTPMiddleware): async def dispatch( self, request: Request, call_next, ): started_at = time.perf_counter() response = await call_next(request) route = request.scope.get("route") route_path = ( route.path if route else "unmatched" ) status_class = ( f"{response.status_code // 100}xx" ) HTTP_REQUESTS_TOTAL.labels( method=request.method, route=route_path, status_class=status_class, ).inc() HTTP_REQUEST_DURATION.labels( method=request.method, route=route_path, ).observe( time.perf_counter() - started_at ) return response
Database Metrics
Monitor:
Connection-pool usageActive connectionsWaiting connectionsQuery durationTransaction durationDeadlocksRollback countDatabase sizeReplication delay
A healthy API can still fail when the database pool is exhausted.
Slow Query Logging
Define a threshold.
Example:
Queries slower than 500 ms:WarningQueries slower than 2 seconds:Error or performance event
Log:
{ "event": "slow_database_query", "duration_ms": 1842, "query_name": "list_ranked_opportunities", "organization_id": 10}
Do not log full SQL statements containing sensitive parameter values by default.
Background-Job Metrics
Track:
Jobs queuedJobs startedJobs completedJobs failedJob durationQueue depthQueue waiting timeRetry countDead-letter count
Example metrics:
background_jobs_totalbackground_job_duration_secondsbackground_job_retries_totalbackground_queue_depthbackground_job_wait_seconds
Job Context
Every background job should include:
job_idcorrelation_idorganization_idjob_typeattempt_numberqueued_at
Example payload:
{ "job_id": "job_384a...", "correlation_id": "req_7e3b...", "organization_id": 10, "job_type": "tender_analysis", "tender_id": 551, "attempt_number": 1}
This context should appear in logs and traces.
Queue-Latency Monitoring
Job processing time is not the same as total waiting time.
Track:
queued_atstarted_atcompleted_at
Then calculate:
Queue wait:started_at - queued_atProcessing duration:completed_at - started_atEnd-to-end duration:completed_at - queued_at
A worker can be fast while users still wait because the queue is overloaded.
Tender Ingestion Metrics
Track:
Sources polledSource requestsSource failuresTenders discoveredNew tenders storedDuplicate tenders skippedDocuments downloadedParsing failuresLast successful source run
Example:
tender_source_poll_totaltender_source_errors_totaltenders_discovered_totaltenders_ingested_totaltender_duplicates_total
Source Freshness
A tender source can fail silently while the rest of the application remains healthy.
Track:
Last successful ingestion timestamp
Alert when:
No successful ingestion for expected interval
Example:
Source normally updates hourlyAlert if no success for three hours
The threshold should match each source’s normal schedule.
AI Monitoring
AI processing requires dedicated metrics.
Track:
Requests per providerRequests per model familySuccess rateFailure rateTimeoutsRate-limit responsesInput tokensOutput tokensLatencyEstimated costCache hit rateFallback usage
AI Metrics
Example names:
ai_requests_totalai_request_duration_secondsai_request_failures_totalai_input_tokens_totalai_output_tokens_totalai_estimated_cost_minor_totalai_cache_hits_totalai_provider_fallback_total
Use controlled labels:
operationprovidermodel_familystatus
Avoid organization_id as a general metric label if there may be thousands of organizations.
Organization-specific analysis can use database records or logs.
AI Reliability Dashboard
A dashboard could show:
AI Requests per MinuteSuccess RateP50 LatencyP95 LatencyP99 LatencyProvider Error RateFallback RateCache Hit RateEstimated Hourly Cost
This helps identify whether AI-provider problems are affecting users.
Detecting AI Quality Problems
Operational success does not guarantee useful output.
An AI request may return 200 OK but produce:
- Invalid JSON
- Missing fields
- Unsupported claims
- Low-confidence analysis
- Empty summaries
- Hallucinated deadlines
Track application-level quality metrics:
Schema-validation failuresOutput-repair attemptsLow-confidence resultsUser correction rateRegeneration requestsHuman-review rejection rate
Billing Monitoring
Billing workflows require strong visibility.
Track:
Checkout sessions createdCheckout completion rateWebhook events receivedWebhook failuresDuplicate webhook eventsSubscription sync durationPayment failuresPast-due subscriptionsTrial conversions
A delayed webhook can incorrectly restrict a paying customer.
Webhook Processing Metrics
Example:
billing_webhook_events_totalbilling_webhook_processing_secondsbilling_webhook_failures_totalbilling_webhook_retries_totalbilling_webhook_backlog
Alert when unprocessed billing events remain above a safe threshold.
Email Delivery Monitoring
Track:
Emails queuedEmails sentEmails failedRetry countDelivery-provider latencyBounce eventsComplaint events
Separate:
Technical delivery successCustomer inbox deliveryUser engagement
Sending an email successfully does not guarantee that it reached the inbox.
Slack and Teams Monitoring
Track:
Messages queuedMessages deliveredWebhook failuresRate limitsDisabled integrationsInvalid credentialsAverage delivery latency
Repeated authentication failures may indicate that a customer revoked an integration.
Authentication Metrics
Track:
Login attemptsLogin successesLogin failuresRefresh failuresSession revocationsInvitation acceptance failuresPassword reset requestsRate-limit events
Security-sensitive metrics should be monitored without storing passwords or token values.
Tenant-Aware Operational Analysis
Operators may need to understand whether one organization is causing unusual load.
Useful database-backed reports include:
Requests per organizationAI cost per organizationFailed jobs per organizationStorage usage per organizationNotification volume per organization
Avoid exposing other tenants’ information to customer-facing dashboards.
Internal operational access should be tightly restricted and audited.
Distributed Tracing
Distributed tracing follows one operation through multiple components.
Example:
POST /tenders/{id}/analyze | +---- Authenticate user | +---- Check entitlement | +---- Reserve usage | +---- Queue AI job | +---- Load document | +---- Check cache | +---- Call AI provider | +---- Validate output | +---- Save analysis | +---- Confirm usage
Each step becomes a span.
Trace Structure
Trace├── HTTP request span├── Authentication span├── Database query span├── Entitlement check span├── Queue publish span├── Worker processing span├── AI-provider span└── Database write span
A trace exposes which span consumed the most time.
Tracing Module
Create:
app/observability/tracing.py
A provider-neutral implementation can use an open telemetry standard.
Example initialization:
def configure_tracing( *, service_name: str, environment: str, exporter_endpoint: str,): resource = Resource.create( { "service.name": service_name, "deployment.environment": environment, } ) provider = TracerProvider( resource=resource ) processor = BatchSpanProcessor( OTLPSpanExporter( endpoint=exporter_endpoint ) ) provider.add_span_processor( processor ) trace.set_tracer_provider(provider)
The exact instrumentation packages depend on the selected observability stack.
Manual Spans
Automatic instrumentation covers many framework operations.
Business workflows still need custom spans.
tracer = trace.get_tracer(__name__)def analyze_tender( tender_id: int, organization_id: int,): with tracer.start_as_current_span( "tender_analysis" ) as span: span.set_attribute( "tender.id", tender_id, ) span.set_attribute( "organization.id", organization_id, ) return orchestrator.analyze( tender_id=tender_id, organization_id=organization_id, )
Be cautious when placing high-cardinality attributes in systems that charge based on indexed fields.
Trace Sampling
Recording every trace may be expensive at high traffic volumes.
Use sampling.
Example strategy:
100% of errors100% of slow requests100% of billing workflows100% of security-sensitive workflows10% of ordinary successful requests
Adaptive sampling can preserve important traces without excessive cost.
Error Tracking
Error tracking should capture unhandled exceptions automatically.
Useful context:
Exception typeStack traceRelease versionEnvironmentRequest pathCorrelation IDTrace IDOrganization IDUser IDJob type
Sensitive fields must be removed before transmission.
Expected Versus Unexpected Errors
Not every error response belongs in an error tracker.
Expected:
Invalid passwordUsage limit reachedPermission deniedTender not found
Unexpected:
Database connection droppedAttribute errorUnexpected provider responseTransaction corruptionUncaught serialization failure
Capture unexpected exceptions automatically.
Record expected business failures as structured events or metrics.
Global Exception Handler
Create a FastAPI exception handler.
from fastapi import Requestfrom fastapi.responses import JSONResponseapp.exception_handler(Exception)async def unhandled_exception_handler( request: Request, exception: Exception,): logger.exception( "unhandled_application_exception", extra={ "request_method": request.method, "request_path": request.url.path, }, ) error_tracker.capture_exception( exception ) return JSONResponse( status_code=500, content={ "detail": { "code": "internal_server_error", "message": "An unexpected error occurred.", "correlation_id": correlation_id_context.get(), } }, )
Never expose stack traces to production clients.
Release Tracking
Every log, error, metric deployment marker, and trace should identify the release.
Example:
release_version = git commit SHA
This allows operators to answer:
Did the error rate increase after deployment 8f6b2c1?
Release markers should appear on dashboards.
Health Endpoints
The application needs separate endpoints for different purposes.
Recommended:
GET /health/liveGET /health/readyGET /health/details
These endpoints should not all expose the same information.
Liveness Endpoint
Liveness answers:
Is the application process alive?
Example:
router.get("/health/live")def liveness(): return { "status": "alive", }
It should be lightweight and should not depend on external services.
A temporary database failure should not necessarily cause the process to restart repeatedly.
Readiness Endpoint
Readiness answers:
Can this instance safely receive traffic?
Check critical dependencies:
- Database connectivity
- Required configuration
- Migration compatibility
- Queue availability, if mandatory
Example:
router.get("/health/ready")def readiness( db: Session = Depends(get_db),): checks = { "database": check_database(db), "configuration": check_required_configuration(), } ready = all( item["status"] == "ok" for item in checks.values() ) return JSONResponse( status_code=200 if ready else 503, content={ "status": "ready" if ready else "not_ready", "checks": checks, }, )
Detailed Health Endpoint
A detailed endpoint can show:
DatabaseQueueObject storageAI providerEmail providerBilling providerTender sources
This endpoint should be restricted.
It may reveal infrastructure details that should not be publicly available.
Dependency Health
Classify dependencies.
Critical:DatabaseAuthentication configurationPrimary queueDegraded-mode:Secondary AI providerEmail providerSlack deliveryOptional tender source
Failure of an optional dependency should not always mark the entire API as unavailable.
Readiness and Database Migrations
A new application version may require a newer database schema.
During startup, verify that the expected migration version is present.
Application releaseexpectsdatabase revision X
A mismatch should prevent the incompatible instance from accepting traffic.
Synthetic Monitoring
Synthetic monitoring runs automated checks from outside the application.
Examples:
Open login pageCall public API endpointPerform test authenticationRun a safe tender-search requestVerify response timeVerify expected page content
This confirms that the platform works from a customer’s perspective.
Server metrics alone cannot detect every frontend, DNS, certificate, or routing failure.
Uptime Checks
Monitor:
HomepageAPI livenessAPI readinessAuthentication endpointCritical customer workflow
Checks should run from more than one geographic region where practical.
This helps distinguish local network failures from platform-wide outages.
Frontend Observability
The React frontend also needs monitoring.
Track:
Page-load performanceJavaScript errorsFailed API requestsRoute-transition failuresAuthentication-refresh failuresUser-visible rendering errors
Frontend problems may not appear in backend logs.
React Error Boundary
Create:
src/observability/AppErrorBoundary.jsx
Example:
import React from "react";export class AppErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, }; } static getDerivedStateFromError() { return { hasError: true, }; } componentDidCatch(error, errorInfo) { errorTracker.captureException( error, { extra: errorInfo, } ); } render() { if (this.state.hasError) { return ( <div> <h1> Something went wrong </h1> <p> Refresh the page or contact support if the problem continues. </p> </div> ); } return this.props.children; }}
Do not display technical stack traces to users.
Frontend Request Correlation
The browser can generate or reuse correlation IDs for API requests.
api.interceptors.request.use(config => { config.headers[ "X-Correlation-ID" ] = createCorrelationId(); return config;});
For workflows involving several requests, reuse one workflow identifier where appropriate.
Browser Performance Metrics
Useful measurements include:
Largest Contentful PaintInteraction latencyCumulative layout shiftInitial bundle load timeAPI request durationFrontend route load time
Performance targets should reflect actual user workflows rather than vanity metrics.
Service-Level Indicators
A Service-Level Indicator, or SLI, measures service behavior.
Examples:
AvailabilityLatencyError rateData freshnessJob completion rateNotification delivery rate
An SLI must be measurable.
Service-Level Objectives
A Service-Level Objective, or SLO, defines a target for an SLI.
Examples:
API availability:99.9% per monthAPI latency:95% of requests below 500 msTender ingestion:95% of scheduled source runs succeedAI analysis:95% complete within 5 minutesEmail notifications:99% queued within 2 minutes
These targets should be realistic and aligned with customer expectations.
Example API Availability SLO
Define successful requests:
HTTP responsesexcludingexpected 4xx client errors
Availability:
Successful eligible requestsdivided byall eligible requests
Do not count customer validation mistakes as platform outages.
Error Budgets
An error budget is the amount of unreliability allowed by an SLO.
Example:
Monthly availability target:99.9%Allowed unavailability:Approximately 43 minutes in a 30-day month
When the error budget is exhausted:
- Slow feature releases
- Prioritize reliability
- Investigate recurring failures
- Improve tests and architecture
Error budgets help balance product speed and operational stability.
Alerting Philosophy
An alert should indicate that human action is needed.
Bad alert:
One request returned 500
Better alert:
API 5xx error rate exceeded 5% for 10 minutes
Individual failures belong in logs and error tracking.
Alerts should focus on sustained or severe conditions.
Alert Severity Levels
Define a consistent severity model.
SEV-1Platform unavailable or major security incidentSEV-2Critical customer workflow seriously degradedSEV-3Limited functionality or rising operational riskSEV-4Non-urgent issue requiring planned investigation
Each severity level should define response expectations.
SEV-1 Examples
All API traffic failingDatabase unavailableCross-tenant data exposure suspectedAuthentication system unavailableBilling incorrectly charging many customers
These require immediate response.
SEV-2 Examples
AI analysis failing for most customersTender ingestion stopped across major sourcesLarge webhook backlogEmail delivery unavailableSubscription access incorrectly restricted
SEV-3 Examples
One provider degraded but failover worksA non-critical source has stopped updatingQueue latency is increasingCache hit rate has dropped significantly
Alert Routing
Different alerts should reach different owners.
API and database alertsEngineering operationsAI-provider alertsAI platform ownerBilling alertsFinance and engineeringSecurity alertsSecurity ownerTender-source alertsData ingestion owner
Every alert needs:
- Owner
- Severity
- Runbook
- Escalation path
Alert Channels
Possible channels:
On-call notification platformSMSPhoneEmailSlack or Teams incident channel
Critical alerts should not rely solely on email.
Alert Deduplication
A single database outage may cause:
- API errors
- Worker failures
- Billing failures
- Notification failures
Without grouping, operators may receive hundreds of alerts.
Use:
- Deduplication keys
- Alert grouping
- Dependency-aware suppression
- Cooldown windows
The primary root-cause alert should remain visible.
Example Alert Rules
API Availability
Condition:5xx rate exceeds 5%Duration:10 minutesSeverity:SEV-2
Database Connections
Condition:Connection pool usage above 90%Duration:5 minutesSeverity:SEV-2
Queue Backlog
Condition:Oldest critical job waiting over 10 minutesDuration:5 minutesSeverity:SEV-2
AI Provider
Condition:Primary-provider error rate above 20%Duration:5 minutesSeverity:SEV-3 if failover worksSEV-2 if failover fails
Dead-Letter Queue Monitoring
Jobs that exceed retry limits should move to a dead-letter queue.
Track:
Dead-letter job countJob typeOldest job ageFailure reason
Alert when critical jobs enter the queue.
Do not silently discard failed jobs.
Operational Event Model
Some events deserve durable application storage in addition to logs.
Create:
app/models/operational_event.py
Fields:
idorganization_idevent_typeseverityservice_nameresource_typeresource_idcorrelation_idevent_metadataoccurred_atresolved_at
Use this selectively for:
- Persistent processing failures
- Customer-visible degraded operations
- Manual intervention requirements
- Reconciliation failures
Do not duplicate every log entry in the database.
Operational Event Status
class OperationalEventStatus( str, Enum,): OPEN = "open" ACKNOWLEDGED = "acknowledged" RESOLVED = "resolved" IGNORED = "ignored"
This supports an internal operations dashboard.
Internal Operations Dashboard
The internal dashboard can display:
Platform StatusOpen IncidentsFailed Background JobsDead-Letter QueueTender Source FreshnessAI Provider HealthBilling Webhook BacklogEmail Delivery StatusRecent Deployments
This dashboard must not be accessible to ordinary organization users.
Customer-Facing Status Information
Customers may benefit from a limited status view.
Examples:
Tender analysis delayedEmail notifications degradedScheduled maintenanceResolved incident
Avoid exposing:
- Internal hostnames
- Provider credentials
- Customer identities
- Detailed attack information
- Sensitive architecture details
Incident Model
Create:
incidents
Recommended fields:
idtitlestatusseveritysummarystarted_atdetected_atacknowledged_atresolved_atincident_commander_user_idcustomer_impactroot_causeresolutioncreated_atupdated_at
Incident Lifecycle
Detected | vAcknowledged | vInvestigating | vMitigated | vResolved | vPost-Incident Review
The exact states can be stored in an enum.
Incident Response Workflow
A basic workflow:
1. Confirm the alert2. Assign severity3. Identify incident owner4. Assess customer impact5. Mitigate immediate damage6. Communicate status7. Resolve root cause8. Verify recovery9. Write post-incident review10. Track follow-up actions
The priority during an incident is restoring safe service, not proving the root cause immediately.
Runbooks
Every actionable alert should link to a runbook.
A runbook should contain:
Alert meaningPossible causesDiagnostic queriesRelevant dashboardsSafe mitigation stepsRollback instructionsEscalation contactsVerification steps
Example runbook:
Alert:AI provider error rate highCheck:Provider statusRate-limit logsCredential validityFallback statusMitigate:Route traffic to secondary providerReduce concurrencyPause non-critical batch jobs
Deployment Monitoring
Every deployment should create an observability event.
Track:
Release versionDeployment startDeployment endEnvironmentChanged servicesMigration versionDeployment result
Dashboard annotations help correlate changes with incidents.
Canary Releases
For high-risk changes:
Deploy to small percentage↓Observe metrics↓Continue or rollback
Monitor:
- Error rate
- Latency
- AI-output validation
- Database load
- Queue failures
Do not promote the release automatically when key indicators regress.
Rollback Signals
Automatic or manual rollback may be triggered by:
Error rate increaseReadiness failuresSevere latency regressionDatabase migration incompatibilityAuthentication failure spikeCross-tenant security test failure
Some database migrations cannot be rolled back safely and require forward fixes.
Capacity Monitoring
Track resource saturation.
CPU usageMemory usageDisk usageDatabase storageDatabase connectionsQueue depthWorker concurrencyObject-storage growthNetwork throughput
Utilization alone is insufficient.
Track whether resource limits are approaching customer-impact thresholds.
Capacity Forecasting
Use historical trends to estimate:
When database storage will reach 80%When queue workers need scalingWhen AI budgets need adjustmentWhen log storage costs will increaseWhen object storage requires lifecycle cleanup
Capacity planning should happen before an outage.
Autoscaling Signals
Possible scaling signals:
API CPUActive requestsRequest latencyQueue depthOldest job ageWorker utilization
For background workers, queue waiting time may be more useful than CPU alone.
Business Metrics and Operational Metrics
Do not confuse business analytics with system observability.
Business metrics:
New organizationsTrial conversionsTenders savedSubscriptions upgraded
Operational metrics:
API errorsJob failuresQueue latencyDatabase saturation
They can appear on related dashboards but serve different purposes.
Domain-Specific Success Metrics
For the Tender Opportunity Scanner, track whether the product is functioning end-to-end.
Examples:
Tenders discovered per sourceSuccessful tender analysesMatching jobs completedHigh-score opportunities generatedDeadline notifications sentSaved opportunities created
A technically healthy platform that discovers zero tenders is not delivering value.
Data Freshness Dashboard
Display:
SourceLast successful pollLast tender discoveredExpected frequencyCurrent statusFailure count
Example:
TED EuropeLast poll:12 minutes agoExpected:HourlyStatus:Healthy
End-to-End Workflow Metrics
Track complete workflows rather than only individual services.
Example tender-processing latency:
Tender discovered↓Document downloaded↓Content parsed↓AI analysis completed↓Match generated↓Customer notified
Measure the total time from discovery to customer notification.
This is often more meaningful than isolated API latency.
Trace and Log Linking
Logs should include:
trace_idspan_id
The log platform can then link an error log to the related trace.
The trace can link to:
- Database spans
- Provider calls
- Queue processing
- Worker retries
This reduces investigation time.
Metrics Exemplars
Where supported, a metric spike can link to a representative trace.
Example:
P99 latency increased↓Open example trace↓AI-provider call took 18 seconds
This connects high-level monitoring to detailed diagnostics.
Audit Logs Versus Application Logs
Audit logs and operational logs serve different purposes.
Application logs:
Help debug and operate the platform
Audit logs:
Record who performed important actions
Audit events should include:
- Actor
- Organization
- Action
- Target
- Timestamp
- Result
They often require stronger retention and tamper protection.
Security Monitoring
Monitor for:
Repeated login failuresSuspicious token refreshesInvitation abusePermission-denied spikesCross-tenant access attemptsUnusual exportsUnexpected billing changesAPI-key misuseWebhook signature failures
A single denied request may be harmless.
Patterns over time may indicate abuse.
Cross-Tenant Access Alerts
Tenant-isolation failures are critical.
Log every attempted cross-tenant access only when it can be distinguished from an ordinary missing record.
Possible event:
{ "event": "cross_tenant_access_blocked", "severity": "critical", "actor_organization_id": 10, "target_organization_id": 25, "resource_type": "saved_opportunity", "resource_id": 551, "correlation_id": "req_..."}
Be careful not to reveal the target organization to the requester.
Internally, this may require security investigation.
Privacy and Observability
Observability systems process potentially sensitive metadata.
Apply:
Data minimizationPurpose limitationAccess controlRetention limitsEncryptionAudit loggingRegional storage requirements
Observability platforms should not become uncontrolled copies of customer data.
Access Control for Operational Tools
Operational dashboards may contain:
- Organization identifiers
- User identifiers
- System architecture
- Billing status
- Failure details
Restrict access using:
Single sign-onMulti-factor authenticationLeast privilegeRole separationAccess reviewsAudit logging
Do not share one administrator account across the team.
Environment Separation
Keep observability data separated by environment.
DevelopmentTestingStagingProduction
Every signal should include:
deployment.environment
A staging error must not trigger a production incident.
Local Development Observability
Developers should be able to test observability locally.
Possible local stack:
FastAPIBackground WorkerMetrics CollectorDashboardTrace ViewerLog Viewer
A minimal local environment can be started through Docker Compose.
Docker Compose Example
services: api: build: ./backend environment: ENVIRONMENT: development LOG_FORMAT: json METRICS_ENABLED: "true" TRACING_ENABLED: "true" metrics: image: prom/prometheus volumes: - ./observability/prometheus.yml: /etc/prometheus/prometheus.yml dashboard: image: grafana/grafana ports: - "3000:3000"
Exact services depend on the selected observability tools.
Application Configuration
Add:
ENVIRONMENT=developmentSERVICE_NAME=tender-apiRELEASE_VERSION=localLOG_LEVEL=INFOLOG_FORMAT=jsonMETRICS_ENABLED=trueMETRICS_PATH=/internal/metricsTRACING_ENABLED=trueOTEL_EXPORTER_ENDPOINT=http://collector:4317ERROR_TRACKING_ENABLED=trueERROR_TRACKING_DSN=...
Never commit production credentials.
Configuration Model
class ObservabilitySettings( BaseSettings): environment: str = "development" service_name: str = "tender-api" release_version: str = "local" log_level: str = "INFO" log_format: str = "json" metrics_enabled: bool = True metrics_path: str = ( "/internal/metrics" ) tracing_enabled: bool = False otel_exporter_endpoint: ( str | None ) = None error_tracking_enabled: bool = False error_tracking_dsn: str | None = None
Validate required settings at startup.
Protecting the Metrics Endpoint
The metrics endpoint can expose:
- Route names
- Service behavior
- Infrastructure state
- Error rates
Do not expose it publicly without protection.
Possible strategies:
Private network onlyInternal authenticationCollector scraping from trusted subnetService mesh policy
Application Startup
Initialize observability before most application services.
settings = load_settings()configure_logging( service_name=settings.service_name, environment=settings.environment, release_version=( settings.release_version ), log_level=settings.log_level,)configure_tracing( service_name=settings.service_name, environment=settings.environment, exporter_endpoint=( settings.otel_exporter_endpoint ),)configure_error_tracking( dsn=settings.error_tracking_dsn, environment=settings.environment, release_version=( settings.release_version ),)
Startup failures should be logged clearly before the application exits.
Operational Dashboards
Create several focused dashboards instead of one overloaded screen.
Recommended dashboards:
Platform OverviewAPI PerformanceDatabase HealthBackground JobsTender IngestionAI OperationsNotificationsBilling and WebhooksSecurityBusiness Health
Platform Overview Dashboard
Include:
Current availabilityRequest volumeError rateP95 latencyOpen incidentsQueue backlogDatabase healthAI-provider statusLast deployment
This should answer:
Is the platform healthy right now?
API Performance Dashboard
Display:
Requests per secondResponse status distributionP50 latencyP95 latencyP99 latencySlowest routesRate-limit eventsAuthentication failures
Use route templates rather than raw URLs.
Background Jobs Dashboard
Display:
Queue depthOldest waiting jobJobs completed per minuteFailure rateRetry rateDead-letter countProcessing duration by job typeWorker availability
AI Operations Dashboard
Display:
Requests by operationRequests by providerModel-family distributionSuccess rateP95 latencyInput and output tokensEstimated costCache hit rateFallback rateOutput-validation failures
Billing Operations Dashboard
Display:
Webhook events receivedWebhook processing failuresUnprocessed event backlogCheckout completion ratePayment failuresSubscription synchronization delayTrial conversionsPast-due organizations
Dashboard Time Windows
Support:
Last 15 minutesLast hourLast 24 hoursLast 7 daysCustom range
Operational dashboards should default to a recent window.
Business dashboards may use longer periods.
Alert Testing
Alerts should be tested intentionally.
Examples:
Generate controlled API errorsPause one workerBlock a test dependencyInsert a dead-letter test jobSend an invalid webhook signatureTrigger a staging payment failure
Do not wait for a production incident to discover that alerts are misconfigured.
Preventing Alert Fatigue
Alert fatigue occurs when too many low-value notifications are generated.
Reduce it by:
- Raising thresholds appropriately
- Requiring sustained conditions
- Grouping related alerts
- Removing non-actionable alerts
- Assigning clear ownership
- Reviewing noisy alerts after incidents
An ignored alert is effectively no alert.
Operational Reviews
Run a regular operational review covering:
IncidentsSLO performanceError-budget consumptionNoisy alertsSlow queriesCapacity trendsAI-provider reliabilityBilling failuresSecurity anomaliesUpcoming risks
This turns observability data into engineering improvements.
Post-Incident Review
A post-incident review should include:
Incident summaryCustomer impactTimelineDetection methodRoot causeContributing factorsWhat workedWhat failedCorrective actionsOwners and deadlines
The purpose is system improvement, not blame.
Operational Data Retention
Example retention matrix:
| Data Type | Example Retention |
|---|---|
| High-volume metrics | 30–90 days |
| Aggregated metrics | 12–24 months |
| Application logs | 30–90 days |
| Security logs | 12 months |
| Traces | 7–30 days |
| Error groups | 6–12 months |
| Audit events | Contractual or legal requirement |
| Incident reports | Long-term |
Actual retention depends on cost, contracts, and legal requirements.
Testing the Observability Layer
Create:
tests/└── observability/ ├── test_logging_context.py ├── test_redaction.py ├── test_correlation_middleware.py ├── test_metrics_middleware.py ├── test_health_endpoints.py ├── test_error_handler.py ├── test_job_instrumentation.py └── test_sensitive_data_exclusion.py
Correlation Middleware Test
def test_response_contains_correlation_id( client,): response = client.get( "/health/live" ) assert response.status_code == 200 assert ( "X-Correlation-ID" in response.headers )
Incoming Correlation ID Test
def test_valid_incoming_correlation_id_is_reused( client,): response = client.get( "/health/live", headers={ "X-Correlation-ID": "req_test123" }, ) assert ( response.headers[ "X-Correlation-ID" ] == "req_test123" )
Redaction Test
def test_sensitive_fields_are_redacted(): payload = { "email": "user@example.com", "password": "secret-value", "access_token": "token-value", } result = redact_mapping(payload) assert result["email"] == ( "user@example.com" ) assert result["password"] == ( "[REDACTED]" ) assert result["access_token"] == ( "[REDACTED]" )
Health Endpoint Tests
Test:
Liveness returns 200Readiness returns 200 when dependencies workReadiness returns 503 when database is unavailableDetailed health requires authorizationSensitive configuration is not returned
Metrics Tests
Test:
- Request counter increases
- Status classes are labeled correctly
- Route templates are used
- Duration histogram records requests
- Unmatched routes are handled
- Organization IDs are not added as high-cardinality labels
Background Job Instrumentation Test
def test_failed_job_records_metrics_and_logs( job_runner, metrics_registry, captured_logs,): with pytest.raises( TemporaryProcessingError ): job_runner.run( job_type="tender_analysis", job_id="job_test", organization_id=10, ) assert metrics_registry.counter_value( "background_jobs_total", labels={ "job_type": "tender_analysis", "status": "failed", }, ) == 1 assert any( log["event"] == "background_job_failed" for log in captured_logs )
Sensitive Data Exclusion Tests
Test that logs and error events never contain:
PasswordsAuthentication tokensAPI keysBilling secretsRaw tender contentPrivate internal notesFull request bodies
This should be part of automated security testing.
Load Testing
Observability must remain useful under load.
Run tests that generate:
- High API traffic
- Many concurrent AI jobs
- Large queue backlogs
- Database saturation
- Provider timeouts
Verify:
Metrics remain accurateLogs are not dropped unexpectedlyTracing overhead is acceptableAlerts trigger correctlyApplication latency remains within target
Observability Overhead
Instrumentation consumes resources.
Measure:
CPU overheadMemory overheadNetwork trafficLog volumeTrace volumeMetric cardinalityStorage cost
The observability system should not become the cause of an outage.
Failure of the Observability Platform
The application should continue operating when the external observability service is temporarily unavailable.
Use:
Buffered exportersBatch sendingShort timeoutsNon-blocking log deliveryGraceful failure
Do not make ordinary user requests wait for remote telemetry delivery.
End-to-End Operational Scenario
A production workflow might look like this:
1. Tender source poll begins2. Correlation and job IDs are created3. Source request succeeds4. Twelve new tenders are discovered5. Tender-analysis jobs enter the queue6. Queue depth metric increases7. Workers start processing8. AI provider latency rises9. Primary provider returns rate limits10. Model router uses secondary provider11. Fallback metric increases12. One job exceeds retry limit13. Job moves to dead-letter queue14. Alert opens15. Operator opens the runbook16. Trace shows repeated provider timeouts17. Provider routing is adjusted18. Failed job is replayed19. Metrics return to normal20. Incident is resolved
Without observability, operators would only know that one tender analysis was missing.
With observability, the complete chain is visible.
Production Readiness Checklist
Before launch, verify:
✔ Structured JSON logging is enabled✔ Correlation IDs connect requests and jobs✔ Sensitive values are redacted✔ Logs are collected centrally✔ API metrics are available✔ Database metrics are monitored✔ Background queues are monitored✔ Tender-source freshness is monitored✔ AI providers are monitored✔ Billing webhooks are monitored✔ Email and collaboration delivery are monitored✔ Distributed tracing is configured✔ Unexpected exceptions reach error tracking✔ Release versions are attached to telemetry✔ Liveness and readiness checks are separate✔ Detailed health information is restricted✔ SLOs are defined✔ Critical alerts have runbooks✔ Alert ownership is documented✔ Dead-letter queues are visible✔ Incident response procedures exist✔ Retention policies are configured✔ Operational access is protected✔ Observability failure does not block requests✔ Alert rules have been tested
Current Architecture
Tender Sources | vIngestion Services | vTender Database | vAI Orchestration | vMatching and Opportunity Workflows | vCustomer Notifications | vBilling and Usage ManagementAll Components | +---- Structured Logs +---- Metrics +---- Traces +---- Error Events | vObservability Platform | +---- Dashboards +---- Alerts +---- Incidents +---- Runbooks
The application is no longer a black box.
Operators can understand its health, performance, cost, and customer impact.
Current Deliverables
At this stage, we have designed:
✔ Structured application logging✔ Standard event naming✔ Request correlation IDs✔ Background-job context✔ Sensitive-data redaction✔ Centralized log collection✔ Log-level guidelines✔ Log-retention policies✔ API performance metrics✔ Database metrics✔ Background-job metrics✔ Queue-latency monitoring✔ Tender-source freshness monitoring✔ AI-provider monitoring✔ Billing-webhook monitoring✔ Email-delivery monitoring✔ Slack and Teams monitoring✔ Authentication metrics✔ Distributed tracing✔ Custom business spans✔ Error tracking✔ Release tracking✔ Liveness endpoints✔ Readiness endpoints✔ Restricted detailed health checks✔ Synthetic monitoring✔ Frontend error monitoring✔ Service-level indicators✔ Service-level objectives✔ Error budgets✔ Alert severity levels✔ Alert routing✔ Incident response workflows✔ Operational dashboards✔ Security monitoring✔ Capacity monitoring✔ Runbooks✔ Post-incident reviews✔ Observability tests✔ Production-readiness checklist
The Tender Opportunity Scanner now has the operational visibility required for production use.
Recommended Git Commit
git add .git commit -m "Add production monitoring logging alerting and observability"
Push the changes:
git push origin main
What We Will Build Next
The platform is now observable and operationally manageable.
The next major step is making deployments repeatable, safe, and automated.
We will introduce:
- Production Docker images
- Environment-specific configuration
- Continuous integration
- Automated testing
- Container security scanning
- Database migration workflows
- Continuous deployment
- Staging environments
- Deployment approvals
- Canary releases
- Rollback procedures
- Infrastructure provisioning
- Release versioning
The next article in the development series is:
Conclusion
Production observability transforms the Tender Opportunity Scanner from a feature-complete application into an operable SaaS platform.
Structured logging gives every important event a consistent and searchable representation. Correlation IDs connect browser requests, API calls, queues, background workers, database operations, and AI-provider requests into one understandable workflow.
Metrics expose availability, latency, throughput, failures, resource saturation, ingestion freshness, AI performance, billing reliability, and notification delivery. Distributed tracing reveals where time is spent across complex workflows, while error tracking groups unexpected failures by environment and release.
Health checks, service-level objectives, error budgets, alert policies, dashboards, runbooks, and incident procedures ensure that operational data leads to effective action. At the same time, redaction, retention controls, and access restrictions prevent the observability platform from becoming a source of customer-data exposure.
With monitoring, logging, alerting, and observability in place, the platform can detect failures before customers report them, identify root causes faster, measure reliability objectively, and scale with greater confidence.
The next step is to automate how each new version of the application is tested, packaged, deployed, verified, and rolled back.