Repository: placeholder-motxt/convert
Pull request #131: feat: Write log to file and view it on Grafana
Author: login: MightyZanark
Created: 2025-04-25T10:45:59Z | Merged: 2025-04-26T05:03:45Z

Part of MOT-112
- Added logger config so that when the image is run, all of the logs should be written to a file, which then will be saved in the Docker host's machine so that recreating the container doesn't delete it.
- Added Grafana, Loki, and Promtail to the local `docker-compose.yml` so that we can check if it is working as intended. All you need to do if you want to check is `docker compose up -d` and then check `http://localhost:3100/ready` and wait until it says ready. After that, you can create a `Dashboard` in Grafana and choosing Loki as the datasource. On the `query` part, choose the `job` label and `varlogs` like the image below. Now, just turn on `Table view` as shown by the second image.
![image](https://github.com/user-attachments/assets/a7cdb784-8fa0-439b-a140-8669ffc5a64b)
![image](https://github.com/user-attachments/assets/76c08bbc-6457-48ce-8d11-445281827b56)




Diff:
diff --git a/Dockerfile b/Dockerfile
index bc297ac7..bec48898 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,4 +7,10 @@ RUN pip install --no-cache-dir -r requirements.txt
 
 COPY . .
 
-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
+CMD [ \
+    "uvicorn", "app.main:app", \
+    "--host", "0.0.0.0", \
+    "--port", "8000", \
+    "--workers", "4", \
+    "--log-config", "${LOG_CONFIG_PATH:-logger_conf.json}" \
+]
diff --git a/docker-compose.yml b/docker-compose.yml
index bf7a2035..48423930 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,6 +4,8 @@ services:
   motxt-convert:
     build: .
     container_name: motxt-convert
+    volumes:
+      - ./logs:/app/logs
     ports:
       - "8000:8000"
     networks:
@@ -39,6 +41,31 @@ services:
           cpus: "0.25"
           memory: 64M
 
+  loki-convert:
+    image: grafana/loki:latest
+    container_name: loki-convert
+    ports:
+      - "3100:3100"
+    volumes:
+      - ./monitoring/loki-config.yaml:/etc/loki/loki-config.yaml
+    command: -config.file=/etc/loki/loki-config.yaml
+
+  promtail-convert:
+    image: grafana/promtail:latest
+    container_name: promtail-convert
+    volumes:
+      - ./monitoring/promtail-config.yaml:/etc/promtail/promtail-config.yaml
+      - ./logs:/var/log/
+    command: -config.file=/etc/promtail/promtail-config.yaml
+
+  grafana-convert:
+    image: grafana/grafana:latest
+    container_name: grafana-convert
+    ports:
+      - "3000:3000"
+    volumes:
+      - ./monitoring/grafana-config.yaml:/etc/grafana/provisioning/datasources/datasource.yaml
+
 networks:
   monitoring:
     driver: bridge
diff --git a/logger_conf.json b/logger_conf.json
new file mode 100644
index 00000000..192fa5fc
--- /dev/null
+++ b/logger_conf.json
@@ -0,0 +1,51 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "default": {
+            "()": "uvicorn.logging.DefaultFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(message)s",
+            "use_colors": null
+        },
+        "access": {
+            "()": "uvicorn.logging.AccessFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
+        }
+    },
+    "handlers": {
+        "default": {
+            "formatter": "default",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/app/logs/convert.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "encoding": "utf8"
+        },
+        "access": {
+            "formatter": "access",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/app/logs/convert_access.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "encoding": "utf8"
+        }
+    },
+    "loggers": {
+        "uvicorn": {
+            "handlers": [
+                "default"
+            ],
+            "level": "INFO"
+        },
+        "uvicorn.error": {
+            "level": "INFO"
+        },
+        "uvicorn.access": {
+            "handlers": [
+                "access"
+            ],
+            "level": "INFO",
+            "propagate": false
+        }
+    }
+}
diff --git a/monitoring/grafana-config.yaml b/monitoring/grafana-config.yaml
new file mode 100644
index 00000000..2673dcc3
--- /dev/null
+++ b/monitoring/grafana-config.yaml
@@ -0,0 +1,14 @@
+apiVersion: 1
+
+datasources:
+- name: Prometheus
+  type: prometheus
+  url: http://prometheus:9090
+  access: proxy
+  editable: true
+  isDefault: true
+
+- name: Loki
+  type: loki
+  access: proxy
+  url: http://loki:3100
diff --git a/monitoring/loki-config.yaml b/monitoring/loki-config.yaml
new file mode 100644
index 00000000..7c5e5789
--- /dev/null
+++ b/monitoring/loki-config.yaml
@@ -0,0 +1,32 @@
+auth_enabled: false
+
+server:
+  http_listen_port: 3100
+
+common:
+  instance_addr: 127.0.0.1
+  path_prefix: /loki
+  storage:
+    filesystem:
+      chunks_directory: /loki/chunks
+      rules_directory: /loki/rules
+  replication_factor: 1
+  ring:
+    kvstore:
+      store: inmemory
+
+schema_config:
+  configs:
+    - from: 2020-10-24
+      store: tsdb
+      object_store: filesystem
+      schema: v13
+      index:
+        prefix: index_
+        period: 24h
+
+ruler:
+  alertmanager_url: http://localhost:9093
+
+analytics:
+  reporting_enabled: false
diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml
new file mode 100644
index 00000000..93ecc1f8
--- /dev/null
+++ b/monitoring/prometheus.yml
@@ -0,0 +1,7 @@
+global:
+  scrape_interval: 15s
+
+scrape_configs:
+  - job_name: "motxt-convert"
+    static_configs:
+      - targets: ["motxt-convert:8000"]
diff --git a/monitoring/promtail-config.yaml b/monitoring/promtail-config.yaml
new file mode 100644
index 00000000..ce788b1d
--- /dev/null
+++ b/monitoring/promtail-config.yaml
@@ -0,0 +1,17 @@
+server:
+  http_listen_port: 9080
+
+positions:
+  filename: /tmp/positions.yaml
+
+clients:
+  - url: http://loki:3100/loki/api/v1/push
+
+scrape_configs:
+- job_name: system
+  static_configs:
+  - targets:
+      - localhost
+    labels:
+      job: varlogs
+      __path__: /var/log/*.log*
diff --git a/tests/test_logger.py b/tests/test_logger.py
index 48a72ed0..f221d317 100644
--- a/tests/test_logger.py
+++ b/tests/test_logger.py
@@ -1,5 +1,8 @@
+import json
 import logging
+import logging.config
 import os
+from pathlib import Path
 from unittest.mock import AsyncMock, Mock, patch
 
 import pytest
@@ -175,3 +178,88 @@ def test_uvicorn_error_logger_warning_when_unexpected_exception(
                 logging.WARNING,
                 "An error occured: 'somehow key error occured'",
             ) in caplog.record_tuples
+
+
+@pytest.fixture
+def uvicorn_log_config(tmp_path: Path) -> tuple[dict, Path]:
+    """Load the uvicorn logging config with temp directories"""
+    config_path = "logger_conf.json"
+    with open(config_path) as f:
+        config = json.load(f)
+
+    # Update paths to use temp directory
+    log_dir = tmp_path / "logs"
+    log_dir.mkdir()
+
+    config["handlers"]["default"]["filename"] = str(log_dir / "convert.log")
+    config["handlers"]["access"]["filename"] = str(log_dir / "convert_access.log")
+
+    return config, log_dir
+
+
+def test_uvicorn_error_access_log_config_creates_file(
+    uvicorn_log_config: tuple[dict, Path],
+):
+    config, log_dir = uvicorn_log_config
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    uvicorn_error.error("Some error occured!")
+    uvicorn_access.info("%s%s%s%s%s", "127.0.0.1", "GET", "/", "1.1", "200")
+
+    assert (log_dir / "convert.log").exists()
+    assert (log_dir / "convert_access.log").exists()
+
+    with open(log_dir / "convert.log") as f:
+        content = f.read()
+        assert "Some error occured!" in content
+
+    with open(log_dir / "convert_access.log") as f:
+        content = f.read()
+        assert "GET /" in content
+        assert "200" in content
+        assert "127.0.0.1" in content
+
+
+def test_uvicorn_error_access_rotate_when_near_capacity(
+    uvicorn_log_config: tuple[dict, Path],
+):
+    config, log_dir = uvicorn_log_config
+    config["handlers"]["default"]["maxBytes"] = 1024
+    config["handlers"]["access"]["maxBytes"] = 1024
+
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    uvicorn_error.error("A" * 512)
+    uvicorn_error.error("A" * 600)
+    uvicorn_access.info(
+        "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 512), "1.1", "200"
+    )
+    uvicorn_access.info(
+        "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 600), "1.1", "200"
+    )
+
+    assert len(list(log_dir.glob("convert.log.*"))) > 0
+    assert len(list(log_dir.glob("convert_access.log.*"))) > 0
+
+
+def test_uvicorn_error_access_max_backup_five(uvicorn_log_config: tuple[dict, Path]):
+    config, log_dir = uvicorn_log_config
+    config["handlers"]["default"]["maxBytes"] = 1024
+    config["handlers"]["access"]["maxBytes"] = 1024
+
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    for _ in range(10):
+        uvicorn_error.error("A" * 512)
+        uvicorn_access.info(
+            "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 512), "1.1", "200"
+        )
+
+    assert len(list(log_dir.glob("convert.log.*"))) == 5
+    assert len(list(log_dir.glob("convert_access.log.*"))) == 5


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @MightyZanark, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces logging to a file for the `motxt-convert` application and sets up Grafana, Loki, and Promtail for log viewing and monitoring. The logs are written to a file within the Docker container and persisted on the host machine. The `docker-compose.yml` file is updated to include the necessary services for Grafana, Loki, and Promtail. A new logger configuration file (`logger_conf.json`) is added to configure the logging format and file rotation. Additionally, monitoring configurations for Grafana, Loki, Prometheus, and Promtail are added to the `monitoring/` directory. Tests are added to verify the logging configuration.

### Highlights
* **Logging to File**: The application now writes logs to a file, which is persisted on the Docker host to prevent data loss when the container is recreated. The log files are stored in `/app/logs` inside the container, which is mapped to `./logs` on the host.
* **Grafana, Loki, and Promtail Integration**: The `docker-compose.yml` file is updated to include Grafana, Loki, and Promtail services, enabling log aggregation and visualization. These services are configured to work together for monitoring the application logs.
* **Logger Configuration**: A new `logger_conf.json` file is added to configure the logging format, handlers, and loggers for the application. This configuration specifies the use of rotating file handlers for both default and access logs.
* **Monitoring Configurations**: Configuration files for Grafana, Loki, Prometheus, and Promtail are added to the `monitoring/` directory. These configurations define the data sources, scrape configurations, and other settings required for monitoring the application logs.
* **Testing**: New tests are added to verify that the logging configuration creates the log files, writes to them, and rotates them when they reach capacity. The tests also ensure that the maximum number of backup files is maintained.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **Dockerfile**
  * Modified the CMD instruction to include the `--log-config` option, pointing to the `logger_conf.json` file.
* **docker-compose.yml**
  * Added a volume mount for the `./logs` directory to persist logs on the host machine.
  * Added services for Loki, Promtail, and Grafana to enable log aggregation and visualization.
  * Added volume mounts for the monitoring configuration files for Loki and Promtail.
  * Exposed ports for Loki (3100) and Grafana (3000).
* **logger_conf.json**
  * Added a new file to configure the logging format, handlers, and loggers for the application.
  * Configured rotating file handlers for both default and access logs, with a maximum size of 10MB and 5 backup files.
* **monitoring/grafana-config.yaml**
  * Added a new file to configure Grafana's data sources, including Prometheus and Loki.
* **monitoring/loki-config.yaml**
  * Added a new file to configure Loki, including the storage settings and schema configuration.
* **monitoring/prometheus.yml**
  * Added a new file to configure Prometheus to scrape metrics from the `motxt-convert` application.
* **monitoring/promtail-config.yaml**
  * Added a new file to configure Promtail to scrape logs from the `/var/log/` directory and send them to Loki.
* **tests/test_logger.py**
  * Added new tests to verify that the logging configuration creates the log files, writes to them, and rotates them when they reach capacity.
  * Added a fixture to load the uvicorn logging configuration with temporary directories for testing.
  * Added tests to ensure that the maximum number of backup files is maintained.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> Logs flow like a river,
> Through Loki's embrace they shiver,
> Grafana's bright screen,
> A monitoring scene,
> Insights the data deliver.


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T10:46:10Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces logging to a file and sets up Grafana, Loki, and Promtail for log viewing. The changes seem well-structured and include necessary configurations and tests. However, there are a few areas that could be improved for better maintainability and clarity.

### Summary of Findings
* **Docker Compose Volume Mounts**: The docker-compose.yml file mounts the ./logs directory to /app/logs. It's important to ensure that the application has the correct permissions to write to this directory. Consider using a non-root user inside the container or adjusting permissions.
* **Logging Configuration**: The logger_conf.json file specifies the logging configuration. It's good practice to allow customization of log levels and file paths via environment variables to make the application more flexible.
* **Testing Log Rotation**: The test_logger.py file includes tests for log rotation, but it would be beneficial to add a test case that specifically checks if the log files are rotated correctly when they reach the maxBytes limit.

### Merge Readiness
The pull request is almost ready for merging. Addressing the Docker Compose volume mount permissions, allowing customization of log levels and file paths via environment variables, and adding a test case for log rotation would improve the overall quality and maintainability of the code. I am unable to approve this pull request, and recommend that others review and approve this code before merging.
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T10:46:22Z
user: login: MightyZanark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:53:41Z
user: login: MightyZanark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:00:50Z
user: login: MightyZanark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:02:12Z
user: login: DaWanAnOnli
body: Approved from me.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:37:31Z

Review comments:
diff_hunk: @@ -7,4 +7,10 @@ RUN pip install --no-cache-dir -r requirements.txt
 
 COPY . .
 
-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
+CMD [ \
+    "uvicorn", "app.main:app", \
+    "--host", "0.0.0.0", \
+    "--port", "8000", \
+    "--workers", "4", \
+    "--log-config", "logger_conf.json" \
+]
path: Dockerfile
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider using environment variables for the log config path to allow for easier customization without rebuilding the image.

```
CMD [ \
    "uvicorn", "app.main:app", \
    "--host", "0.0.0.0", \
    "--port", "8000", \
    "--workers", "4", \
    "--log-config", "${LOG_CONFIG_PATH:-logger_conf.json}" \
]
```
created_at: 2025-04-25T10:46:21Z
author_association: NONE
diff_hunk: @@ -4,6 +4,8 @@ services:
   motxt-convert:
     build: .
     container_name: motxt-convert
+    volumes:
+      - ./logs:/app/logs
path: docker-compose.yml
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Ensure the application has the correct permissions to write to the ./logs directory. Consider using a non-root user inside the container or adjusting permissions.

```
    volumes:
      - ./logs:/app/logs:rw
```
created_at: 2025-04-25T10:46:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "default": {
+            "()": "uvicorn.logging.DefaultFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(message)s",
+            "use_colors": null
+        },
+        "access": {
+            "()": "uvicorn.logging.AccessFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
+        }
+    },
+    "handlers": {
+        "default": {
+            "formatter": "default",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/app/logs/convert.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "encoding": "utf8"
path: logger_conf.json
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider making the filename configurable via an environment variable. This allows users to easily change the log file location without modifying the configuration file.

```
            "filename": "${LOG_FILE:-/app/logs/convert.log}",
```
created_at: 2025-04-25T10:46:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "default": {
+            "()": "uvicorn.logging.DefaultFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(message)s",
+            "use_colors": null
+        },
+        "access": {
+            "()": "uvicorn.logging.AccessFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
+        }
+    },
+    "handlers": {
+        "default": {
+            "formatter": "default",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/app/logs/convert.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "encoding": "utf8"
+        },
+        "access": {
+            "formatter": "access",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/app/logs/convert_access.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "encoding": "utf8"
+        }
+    },
+    "loggers": {
+        "uvicorn": {
+            "handlers": [
+                "default"
+            ],
+            "level": "INFO"
+        },
+        "uvicorn.error": {
+            "level": "INFO"
+        },
+        "uvicorn.access": {
+            "handlers": [
+                "access"
+            ],
+            "level": "INFO",
+            "propagate": false
path: logger_conf.json
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider making the log level configurable via an environment variable.

```
            "level": "${LOG_LEVEL:-INFO}",
```
created_at: 2025-04-25T10:46:22Z
author_association: NONE
diff_hunk: @@ -175,3 +178,88 @@ def test_uvicorn_error_logger_warning_when_unexpected_exception(
                 logging.WARNING,
                 "An error occured: 'somehow key error occured'",
             ) in caplog.record_tuples
+
+
+@pytest.fixture
+def uvicorn_log_config(tmp_path: Path) -> tuple[dict, Path]:
+    """Load the uvicorn logging config with temp directories"""
+    config_path = "logger_conf.json"
+    with open(config_path) as f:
+        config = json.load(f)
+
+    # Update paths to use temp directory
+    log_dir = tmp_path / "logs"
+    log_dir.mkdir()
+
+    config["handlers"]["default"]["filename"] = str(log_dir / "convert.log")
+    config["handlers"]["access"]["filename"] = str(log_dir / "convert_access.log")
+
+    return config, log_dir
+
+
+def test_uvicorn_error_access_log_config_creates_file(
+    uvicorn_log_config: tuple[dict, Path],
+):
+    config, log_dir = uvicorn_log_config
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    uvicorn_error.error("Some error occured!")
+    uvicorn_access.info("%s%s%s%s%s", "127.0.0.1", "GET", "/", "1.1", "200")
+
+    assert (log_dir / "convert.log").exists()
+    assert (log_dir / "convert_access.log").exists()
+
+    with open(log_dir / "convert.log") as f:
+        content = f.read()
+        assert "Some error occured!" in content
+
+    with open(log_dir / "convert_access.log") as f:
+        content = f.read()
+        assert "GET /" in content
+        assert "200" in content
+        assert "127.0.0.1" in content
+
+
+def test_uvicorn_error_access_rotate_when_near_capacity(
+    uvicorn_log_config: tuple[dict, Path],
+):
+    config, log_dir = uvicorn_log_config
+    config["handlers"]["default"]["maxBytes"] = 1024
+    config["handlers"]["access"]["maxBytes"] = 1024
+
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    uvicorn_error.error("A" * 512)
+    uvicorn_error.error("A" * 600)
+    uvicorn_access.info(
+        "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 512), "1.1", "200"
+    )
+    uvicorn_access.info(
+        "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 600), "1.1", "200"
+    )
+
+    assert len(list(log_dir.glob("convert.log.*"))) > 0
+    assert len(list(log_dir.glob("convert_access.log.*"))) > 0
+
path: tests/test_logger.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

It would be beneficial to add a test case that specifically checks if the log files are rotated correctly when they reach the maxBytes limit.
created_at: 2025-04-25T10:46:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "default": {
+            "()": "uvicorn.logging.DefaultFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(message)s",
+            "use_colors": null
+        },
+        "access": {
+            "()": "uvicorn.logging.AccessFormatter",
+            "fmt": "%(asctime)s %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
+        }
+    },
+    "handlers": {
+        "default": {
+            "formatter": "default",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "/app/logs/convert.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "encoding": "utf8"
path: logger_conf.json
user: login: MightyZanark
body: I don't think that will work since it's a regular JSON file.
created_at: 2025-04-25T11:53:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,3 +178,88 @@ def test_uvicorn_error_logger_warning_when_unexpected_exception(
                 logging.WARNING,
                 "An error occured: 'somehow key error occured'",
             ) in caplog.record_tuples
+
+
+@pytest.fixture
+def uvicorn_log_config(tmp_path: Path) -> tuple[dict, Path]:
+    """Load the uvicorn logging config with temp directories"""
+    config_path = "logger_conf.json"
+    with open(config_path) as f:
+        config = json.load(f)
+
+    # Update paths to use temp directory
+    log_dir = tmp_path / "logs"
+    log_dir.mkdir()
+
+    config["handlers"]["default"]["filename"] = str(log_dir / "convert.log")
+    config["handlers"]["access"]["filename"] = str(log_dir / "convert_access.log")
+
+    return config, log_dir
+
+
+def test_uvicorn_error_access_log_config_creates_file(
+    uvicorn_log_config: tuple[dict, Path],
+):
+    config, log_dir = uvicorn_log_config
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    uvicorn_error.error("Some error occured!")
+    uvicorn_access.info("%s%s%s%s%s", "127.0.0.1", "GET", "/", "1.1", "200")
+
+    assert (log_dir / "convert.log").exists()
+    assert (log_dir / "convert_access.log").exists()
+
+    with open(log_dir / "convert.log") as f:
+        content = f.read()
+        assert "Some error occured!" in content
+
+    with open(log_dir / "convert_access.log") as f:
+        content = f.read()
+        assert "GET /" in content
+        assert "200" in content
+        assert "127.0.0.1" in content
+
+
+def test_uvicorn_error_access_rotate_when_near_capacity(
+    uvicorn_log_config: tuple[dict, Path],
+):
+    config, log_dir = uvicorn_log_config
+    config["handlers"]["default"]["maxBytes"] = 1024
+    config["handlers"]["access"]["maxBytes"] = 1024
+
+    logging.config.dictConfig(config)
+    uvicorn_error = logging.getLogger("uvicorn.error")
+    uvicorn_access = logging.getLogger("uvicorn.access")
+
+    uvicorn_error.error("A" * 512)
+    uvicorn_error.error("A" * 600)
+    uvicorn_access.info(
+        "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 512), "1.1", "200"
+    )
+    uvicorn_access.info(
+        "%s%s%s%s%s", "127.0.0.1", "GET", "/" + ("A" * 600), "1.1", "200"
+    )
+
+    assert len(list(log_dir.glob("convert.log.*"))) > 0
+    assert len(list(log_dir.glob("convert_access.log.*"))) > 0
+
path: tests/test_logger.py
user: login: MightyZanark
body: Since on the test the `maxBytes` is 1KB, and the logger is only invoked twice such that it will just overflow once, the `log_dir` having files with pattern `convert.log.*` indicates that it properly creates a new file when trying to write the 2nd log. So I don't think this is necessary.
created_at: 2025-04-25T12:00:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,4 +7,10 @@ RUN pip install --no-cache-dir -r requirements.txt
 
 COPY . .
 
-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
+CMD [ \
+    "uvicorn", "app.main:app", \
+    "--host", "0.0.0.0", \
+    "--port", "8000", \
+    "--workers", "4", \
+    "--log-config", "logger_conf.json" \
+]
path: Dockerfile
user: login: MightyZanark
body: Fair point, done
created_at: 2025-04-25T12:02:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-25T10:46:02Z
body: <p><a href="https://linear.app/motxt-scrumboard/issue/MOT-112/extra-4-setup-grafana-prometheus-loki">MOT-112 EXTRA: 4 - Setup Grafana, Prometheus, Loki</a></p>
author_association: NONE
user: login: NtapSlur
created_at: 2025-04-26T05:03:00Z
body: Thanyou for adding this monitoring feature! Approved.
author_association: CONTRIBUTOR