diff --git a/configure.sh b/configure.sh index a2e8d17ee67b49d525617be89d07afd9c7f1e14a..1b42278b3595f0b383b4e6efbb738f5a9c6bdc7a 100755 --- a/configure.sh +++ b/configure.sh @@ -6,6 +6,7 @@ ENABLE_HTTP_ACCESS=${ENABLE_HTTP_ACCESS:="false"} # Configure instance sed -e "s/'disable_existing_loggers': True,/'disable_existing_loggers': False,/" -i pvs_instance/settings.py + sed -e "s/import os/import os\nimport structlog/" -i pvs_instance/settings.py HANDLERS="'handlers': {\n 'null': {\n 'level':'DEBUG',\n @@ -14,7 +15,7 @@ 'console': {\n 'level': 'DEBUG' if DEBUG else 'INFO',\n 'class': 'logging.StreamHandler',\n - 'formatter': 'verbose' if DEBUG else 'simple',\n + 'formatter': 'json',\n 'filters': [],\n },\n }," @@ -25,8 +26,15 @@ 'propagate': False,\n }, }" + FORMATTERS="'formatters': {\n + 'json': {\n + '()': structlog.stdlib.ProcessorFormatter,\n + 'processor': structlog.dev.ConsoleRenderer() if DEBUG else structlog.processors.JSONRenderer(), + }, + }," sed -e "/^ 'handlers': {$/,/^ },$/c `echo ${HANDLERS}`" -i pvs_instance/settings.py sed -e "/^ 'loggers': {$/,/^ }$/c `echo ${LOGGERS}`" -i pvs_instance/settings.py + sed -e "/^ 'formatters': {$/,/^ },$/c `echo ${FORMATTERS}`" -i pvs_instance/settings.py sed -e "s,http_service_url=http://localhost:8000/ows,http_service_url=${APACHE_ALIAS}/ows," -i pvs_instance/conf/eoxserver.conf sed -e "s/resampling_method=average/resampling_method=near/" -i pvs_instance/conf/eoxserver.conf diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 1dcc784778b5a54a47c3d8650fbdcc4859f7ef8a..dd74d2855848ad3d671b69b64c0c729ccfe59a32 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -11,6 +11,12 @@ worker_class = 'sync' # logging accesslog = '-' +access_log_format = '{"remote_address":"%(h)s",' \ + '"response_code":"%(s)s","request_method":"%(m)s",' \ + '"request_path":"%(U)s","request_querystring":"%(q)s",' \ + '"request_time":"%(M)s","response_length":"%(B)s",' \ + '"user":"%(u)s","status_line":"%(r)s","referer":"%(r)s",' \ + '"user_agent":"%(a)s"}' errorlog = '-' loglevel = 'warning' disable_redirect_access_to_syslog = True diff --git a/registrar/backend/eoxserver.py b/registrar/backend/eoxserver.py index 2a1f0aad1c79ef57b9075416e55965c5bdaa61e1..08c652f90561e558037a519605bbde6c4b97c330 100644 --- a/registrar/backend/eoxserver.py +++ b/registrar/backend/eoxserver.py @@ -7,7 +7,7 @@ Contains implementations for different backends where data may be registered """ import os import sys -import logging +import structlog from typing import List, TYPE_CHECKING, Optional, TypedDict, cast import json from urllib.parse import urlparse @@ -26,7 +26,7 @@ from ..source import Source, LocalSource, S3Source, SwiftSource from .abc import ItemBackend -logger = logging.getLogger(__name__) +logger = structlog.getLogger(__name__) class AssetsToCoverageMapping(TypedDict): @@ -207,9 +207,9 @@ class EOxServerBackend(ItemBackend): storage_name = storage.name if created_storage_auth: - logger.info(f"Created storage auth for {source.name}") + logger.info("Created storage auth", source=source.name) if created_storage: - logger.info(f"Created storage for {source.name}") + logger.info("Created storage for", source=source.name) return [storage_name] if storage_name else [] @@ -266,9 +266,9 @@ class EOxServerBackend(ItemBackend): product_type_obj = models.ProductType.objects.get(name=product_type["name"]) if created: - logger.info(f"Created Product Type {product_type_obj}") + logger.info("Created Product Type" , product_type_obj=product_type_obj) else: - logger.info(f"Using existing Product Type {product_type_obj}") + logger.info("Using existing Product Type", product_type_obj=product_type_obj) # resolve storage object if storage: @@ -317,8 +317,8 @@ class EOxServerBackend(ItemBackend): service_visibility.save() logger.info( - f"Successfully {'replaced' if replaced else 'registered'} " - f"Product {product.identifier}" + f"Successfully {'replaced' if replaced else 'registered'} Product", + product=product.identifier, ) return product @@ -362,7 +362,7 @@ class EOxServerBackend(ItemBackend): break if product_type_found: - logger.info(f"Registering into EOxServer for type '{product_type['name']}'") + logger.info("Registering into EOxServer", type=product_type['name']) product = self._register_with_stac( source, item, replace, storage, product_type ) @@ -393,27 +393,29 @@ class EOxServerBackend(ItemBackend): # ugly, ugly hack from eoxserver.resources.coverages import models - try: - logger.info(f"Deregistering product '{identifier}'") - product = models.Product.objects.get(identifier=identifier) - grids = list(models.Grid.objects.filter(coverage__parent_product=product)) - product.delete() - - # clean up grids - for grid in grids: - grid_used = models.EOObject.objects.filter( - Q(coverage__grid=grid) | Q(mosaic__grid=grid), - ).exists() - # clean up grid as well, if it is not referenced - # anymore but saving named (user defined) grids - if grid and not grid.name and not grid_used: - grid.delete() - except models.Product.DoesNotExist: - logger.info(f"No product with identifier '{identifier}' found") - # no product found with that id - # return empty list - return None - - logger.info(f"Deregistered product with identifier '{identifier}'") + with structlog.contextvars.bound_contextvars(indentifier=identifier): + + try: + logger.info("Deregistering product") + product = models.Product.objects.get(identifier=identifier) + grids = list(models.Grid.objects.filter(coverage__parent_product=product)) + product.delete() + + # clean up grids + for grid in grids: + grid_used = models.EOObject.objects.filter( + Q(coverage__grid=grid) | Q(mosaic__grid=grid), + ).exists() + # clean up grid as well, if it is not referenced + # anymore but saving named (user defined) grids + if grid and not grid.name and not grid_used: + grid.delete() + except models.Product.DoesNotExist: + logger.info("No product with identifier found") + # no product found with that id + # return empty list + return None + + logger.info("Deregistered product") # return the deleted identifier return identifier diff --git a/registrar/cli.py b/registrar/cli.py index edfd96e66b67feae55f6aab6b789c6f88439eec7..98698477092da4b3a9eb0911df8229b7bf0e175e 100644 --- a/registrar/cli.py +++ b/registrar/cli.py @@ -2,6 +2,8 @@ from os.path import join, dirname import logging.config import click +import structlog +import structlog.contextvars import yaml import jsonschema @@ -22,13 +24,20 @@ def setup_logging(debug=False): { "version": 1, "disable_existing_loggers": False, - "formatters": {"brief": {"format": "%(levelname)s %(name)s: %(message)s"}}, + "formatters": { + "json": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.dev.ConsoleRenderer() + if debug + else structlog.processors.JSONRenderer(), + }, + }, "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG" if debug else "INFO", - "formatter": "brief", - } + "formatter": "json", + }, }, "root": { "handlers": ["console"], @@ -37,6 +46,25 @@ def setup_logging(debug=False): } ) + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.filter_by_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + context_class=structlog.threadlocal.wrap_dict(dict), + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + def validate_config(config): with open(join(dirname(__file__), "config-schema.yaml")) as f: diff --git a/registrar/daemon.py b/registrar/daemon.py index 1ee95e616e7061579a4da33d0aea0cc7c19532a8..d0239e508451e2852fb9c3369a64507d1ee30270 100644 --- a/registrar/daemon.py +++ b/registrar/daemon.py @@ -6,13 +6,13 @@ Contains the daemon functions for the registrar """ -import logging +import structlog from typing import Callable, Dict import redis -logger = logging.getLogger(__name__) +logger = structlog.getLogger(__name__) # callback declaration: callable with 'config', item and replace @@ -52,8 +52,8 @@ def run_daemon( queue_names = list(handlers.keys()) logger.debug( f"waiting on items on redis " - f"queue{'s' if len(queue_names) > 1 else ''} " - f"{', '.join(queue_names)}..." + f"queue{'s' if len(queue_names) > 1 else ''}", + queues=queue_names, ) while True: diff --git a/registrar/post_handlers.py b/registrar/post_handlers.py index ab8d1b258e813e1ec8b4c55c7a8efec3cad8dfa0..b5da0fd99865716a083101db8e9c0e828c451279 100644 --- a/registrar/post_handlers.py +++ b/registrar/post_handlers.py @@ -8,7 +8,7 @@ Contains different handlers that trigger post registration import os.path import textwrap from datetime import datetime -import logging +import structlog from typing import TYPE_CHECKING from xml.sax.saxutils import escape @@ -17,7 +17,7 @@ if TYPE_CHECKING: from .utils import isoformat -logger = logging.getLogger(__name__) +logger = structlog.getLogger(__name__) class ReportingPostHandler: @@ -36,7 +36,7 @@ class ReportingPostHandler: timestamp = inserted.strftime("%Y%m%dT%H%M%S") sanitized_filename = self.sanitize_path(f"item_{timestamp}_{item.id}.xml") filename = os.path.join(self.reporting_dir, sanitized_filename) - logger.info(f"Generating report for product {item.id} at {filename}") + logger.info("Generating report", product=item.id, filename=filename) identifier = escape(item.id) availability_time = escape(isoformat(inserted)) diff --git a/registrar/registrar.py b/registrar/registrar.py index a266563f958f8f723e0577ee0bdcae1b8264554a..3c94ba8524d9c04b1226e83b1b08be63da80e298 100644 --- a/registrar/registrar.py +++ b/registrar/registrar.py @@ -5,10 +5,11 @@ registrar.py Contains all functions relevant for registration """ -import logging from typing import Optional import json +import structlog +import structlog.contextvars from pystac import Item from .source import get_source, get_source_from_path @@ -16,7 +17,7 @@ from .backend import get_backends, get_path_backends from .exceptions import RegistrationError from .utils import import_by_path -logger = logging.getLogger(__name__) +logger = structlog.getLogger(__name__) def register_item(config: dict, value: str, replace: bool = False): @@ -33,34 +34,36 @@ def register_item(config: dict, value: str, replace: bool = False): not turned on """ item = Item.from_dict(json.loads(value)) - source = get_source(config, item) - logger.info(f"Handling '{item!r}'.") - - for pre_handler in get_pre_handlers(config): - pre_handler(config, item) - - try: - for backend in get_backends(config): - if backend.item_exists(source, item): - if replace: - logger.info(f"Replacing '{item!r}'.") - backend.register_item(source, item, replace=True) + with structlog.contextvars.bound_contextvars(item=item): + logger.info("Handling item") + + source = get_source(config, item) + + for pre_handler in get_pre_handlers(config): + pre_handler(config, item) + + try: + for backend in get_backends(config): + if backend.item_exists(source, item): + if replace: + logger.info("Replacing item") + backend.register_item(source, item, replace=True) + else: + raise RegistrationError(f"Object {item!r} is already registered") else: - raise RegistrationError(f"Object {item!r} is already registered") - else: - logger.info(f"Registering '{item!r}'.") - backend.register_item(source, item, replace=False) - except Exception as e: - for error_handler in get_error_handlers(config): - error_handler(config, item, e) - raise - else: - for post_handler in get_post_handlers(config): - post_handler(config, item) - - logger.info( - f"Successfully {'replaced' if replace else 'registered'} " f"'{item!r}'" - ) + logger.info("Registering item") + backend.register_item(source, item, replace=False) + except Exception as e: + for error_handler in get_error_handlers(config): + error_handler(config, item, e) + raise + else: + for post_handler in get_post_handlers(config): + post_handler(config, item) + + logger.info( + f"Successfully {'replaced' if replace else 'registered'}" + ) def register_path(config: dict, path: str, replace: bool = False): diff --git a/registrar/source.py b/registrar/source.py index cc5d96e507ab160117339b98f34a1a888ce58a40..c4a68b9bd7c995a6ea9298735b701438c1113b36 100644 --- a/registrar/source.py +++ b/registrar/source.py @@ -11,7 +11,7 @@ from os.path import normpath, join, isabs import shutil from glob import glob from fnmatch import fnmatch -import logging +import structlog from typing import TYPE_CHECKING, Optional from abc import ABC, abstractmethod from urllib.parse import urlparse @@ -29,7 +29,7 @@ if TYPE_CHECKING: from swiftclient.service import SwiftService -logger = logging.getLogger(__name__) +logger = structlog.getLogger(__name__) class RegistrationError(Exception): @@ -293,7 +293,7 @@ class S3Source(Source): glob_patterns = [glob_patterns] bucket, key = self.get_container_and_path(path) - logger.info(f"Listing S3 files for bucket {bucket} and prefix {key}") + logger.info("Listing S3 files", bucket=bucket, prefix=key) response = self.client.list_objects_v2( Bucket=bucket, Prefix=key, @@ -312,7 +312,10 @@ class S3Source(Source): def get_file(self, path: str, target_path: str): bucket, key = self.get_container_and_path(path) logger.info( - f"Retrieving file from S3 {bucket}/{key} to be stored at {target_path}" + "Retrieving file from S3", + bucket=bucket, + key=key, + target_path=target_path, ) self.client.download_file(bucket, key, target_path) diff --git a/registrar/xml.py b/registrar/xml.py index e22fc2ddfc6da59aaebec6b97a93ef8e393435cf..52351e79d88d200f3ee6acd18e0c055137525d9c 100644 --- a/registrar/xml.py +++ b/registrar/xml.py @@ -3,7 +3,7 @@ from os.path import join, basename from tempfile import gettempdir from dataclasses import dataclass, field from typing import Optional, Callable, Any -import logging +import structlog import lxml.etree @@ -11,7 +11,7 @@ from .source import Source from .exceptions import RegistrationError -logger = logging.getLogger(__name__) +logger = structlog.getLogger(__name__) def read_xml(source: Source, path: str) -> lxml.etree._ElementTree: diff --git a/requirements.txt b/requirements.txt index 643596764c373cac4bb97e2e6587980f9c321854..14305ff0fcd9715337d2b6ab4a90e16a9130aab1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ pyyaml<6.0 jsonschema<5.0 boto3<2.0 django-cors-headers<4.0 +django-structlog==2.2.0 pystac<2.0 http-access==0.1.3