Files

151 lines
5.1 KiB
Python

import asyncio
import os
import signal
import traceback
from threading import Event
from time import time
from prometheus_client import start_http_server
import ingestor.metrics as metrics
from ingestor.ingestor import Ingestor
exit_signal = Event()
POD_ID = os.getenv('HOSTNAME', 'localhost')
async def main(): # NOSONAR
"""
Main asynchronous function that orchestrates the OPC Ingestor application.
This function performs the following operations:
1. Starts the Prometheus metrics server for monitoring
2. Initializes the Ingestor instance
3. Prepares the ingestor (connects to services, acquires slot leases)
4. Runs the main processing loop until shutdown is requested
5. Handles graceful shutdown and cleanup
The main loop continuously:
- Processes OPC data from subscribed tags
- Manages slot leases and resource allocation
- Monitors OPC server connections
- Records metrics for monitoring and observability
Environment Variables:
HOSTNAME: Pod identifier for metrics labeling (default: "localhost")
HTTP_METRICS_PORT: Port for Prometheus metrics server (default: 9090)
Raises:
Exception: If ingestor preparation fails, the application will exit
"""
start_prometheus_server()
ingestor = Ingestor()
try:
await ingestor.prepare_ingestor()
except Exception as e:
metrics.APP_ERRORS_TOTAL.labels(pod_id=POD_ID).inc() # Increment errors
ingestor.logger.error(f'Failed to prepare ingestor: {e}')
exit_signal.set()
ingestor.logger.info('Ingestor prepared. Starting main loop.')
while not exit_signal.is_set(): # NOSONAR
start_time = time() # Start loop timer
try:
await ingestor.loop()
metrics.APP_LOOP_COUNT.labels(pod_id=POD_ID).inc() # Increment loop counter
# Use asyncio.sleep instead of exit_signal.wait for better async compatibility
await asyncio.sleep(ingestor.poll_interval) # NOSONAR
except KeyboardInterrupt: # Handle Ctrl+C gracefully
print('KeyboardInterrupt received. Setting exit_signal flag.')
exit_signal.set()
except Exception:
print('Exception in main loop. Setting exit_signal flag.')
traceback.print_exc()
metrics.APP_ERRORS_TOTAL.labels(pod_id=POD_ID).inc() # Increment errors
exit_signal.set()
finally:
# Record loop duration
duration = time() - start_time
metrics.APP_LOOP_DURATION.labels(pod_id=POD_ID).observe(duration)
await ingestor.shutdown()
metrics.APP_UP.labels(pod_id=POD_ID).set(0) # Mark app as DOWN
ingestor.logger.info('Main loop exit_signaled.')
# Give Prometheus a chance to scrape one last time before exiting (optional)
await asyncio.sleep(5)
os._exit(0)
def signal_handler(_signum, _frame):
"""
Signal handler for graceful application shutdown.
This function handles system signals (SIGINT, SIGTERM, SIGHUP) by setting
the exit_signal flag, which triggers the main loop to complete its current
iteration and then shut down gracefully.
Args:
_signum: The signal number received
_frame: The current stack frame (unused)
"""
print(f'Received signal {_signum}. Setting exit_signal flag.')
exit_signal.set()
def start_prometheus_server():
"""
Starts the Prometheus metrics HTTP server.
This function initializes a Prometheus metrics server on the configured port
to expose application metrics for monitoring and alerting. The server provides
metrics about application health, performance, and operational status.
Environment Variables:
HTTP_METRICS_PORT: Port number for the metrics server (default: 9090)
Raises:
Exception: If the server fails to start, the application will exit
"""
try:
port = int(os.getenv('HTTP_METRICS_PORT', 9090))
start_http_server(port)
print(f'Prometheus server started on port {port}.')
metrics.APP_UP.labels(pod_id=POD_ID).set(1) # Mark app as UP
except Exception as e:
print(f'Failed to start Prometheus server: {e}')
os._exit(1)
def run_async_main():
"""
Run the async main function with proper event loop setup.
This function sets up the asyncio event loop and runs the main async function.
It handles KeyboardInterrupt gracefully and ensures proper cleanup of the event loop.
The function is designed to work with both direct execution and containerized
environments, providing consistent behavior across different deployment scenarios.
"""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())
except KeyboardInterrupt:
print('KeyboardInterrupt received in main thread.')
exit_signal.set()
finally:
loop.close()
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
run_async_main()