Skip to content

Commit 7432ae0

Browse files
authored
Merge pull request #50 from cybrota/feat/add-logging
feat: add access logging to secret fetches
2 parents 18d619e + 6ee540f commit 7432ae0

File tree

10 files changed

+98
-19
lines changed

10 files changed

+98
-19
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ jobs:
1717

1818
steps:
1919
- name: Checkout repository
20-
uses: actions/checkout@v3
20+
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
2121

2222
- name: Set up Python
23-
uses: actions/setup-python@v4
23+
uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4
2424
with:
2525
python-version: '3.10'
2626

.github/workflows/security.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414

1515
steps:
1616
- name: Checkout repository
17-
uses: actions/checkout@v3
17+
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
1818

1919
- name: Set up Python
20-
uses: actions/setup-python@v4
20+
uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4
2121
with:
2222
python-version: "3.10"
2323

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414

1515
steps:
1616
- name: Checkout repository
17-
uses: actions/checkout@v3
17+
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
1818

1919
- name: Set up Python
20-
uses: actions/setup-python@v4
20+
uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4
2121
with:
2222
python-version: "3.10"
2323

src/whispr/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = "0.7.0"
1+
version = "0.8.0"

src/whispr/aws.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import botocore.exceptions
55
import structlog
66

7+
from whispr.logging import log_secret_fetch
78
from whispr.vault import SimpleVault
8-
from whispr.enums import AWSVaultSubType
99

1010

1111
class AWSVault(SimpleVault):
@@ -29,8 +29,11 @@ def fetch_secrets(self, secret_name: str) -> str:
2929
"""
3030
try:
3131
response = self.client.get_secret_value(SecretId=secret_name)
32+
secret_value = response.get("SecretString") or ""
3233
self.logger.debug(f"Successfully fetched aws secret: {secret_name}")
33-
return response.get("SecretString")
34+
if secret_value:
35+
log_secret_fetch(self.logger, secret_name, "aws")
36+
return secret_value
3437
except botocore.exceptions.ClientError as error:
3538
if error.response["Error"]["Code"] == "ResourceNotFoundException":
3639
self.logger.error(
@@ -74,9 +77,11 @@ def fetch_secrets(self, secret_name: str) -> str:
7477
response = self.client.get_parameter(Name=secret_name)
7578
param = response.get("Parameter")
7679
if param:
77-
return param.get("Value")
78-
else:
79-
return ""
80+
secret_value = param.get("Value") or ""
81+
if secret_value:
82+
log_secret_fetch(self.logger, secret_name, "aws-ssm")
83+
return secret_value
84+
return ""
8085
except botocore.exceptions.ClientError as error:
8186
if error.response["Error"]["Code"] == "ParameterNotFound":
8287
self.logger.error(

src/whispr/azure.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import structlog
44
from azure.keyvault.secrets import SecretClient
55
import azure.core.exceptions
6+
from whispr.logging import log_secret_fetch
67
from whispr.vault import SimpleVault
78

89

@@ -31,6 +32,8 @@ def fetch_secrets(self, secret_name: str) -> str:
3132
try:
3233
secret = self.client.get_secret(secret_name)
3334
self.logger.info(f"Successfully fetched secret: {secret_name}")
35+
if secret.value:
36+
log_secret_fetch(self.logger, secret_name, "azure")
3437
return secret.value
3538
except azure.core.exceptions.ResourceNotFoundError:
3639
self.logger.error(

src/whispr/gcp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import structlog
44
from google.cloud import secretmanager
55
import google.api_core
6+
from whispr.logging import log_secret_fetch
67
from whispr.vault import SimpleVault
78

89

@@ -38,6 +39,8 @@ def fetch_secrets(self, secret_name: str) -> str:
3839
response = self.client.access_secret_version(name=secret_name)
3940
secret_data = response.payload.data.decode("UTF-8")
4041
self.logger.info(f"Successfully fetched gcp secret: {secret_name}")
42+
if secret_data:
43+
log_secret_fetch(self.logger, secret_name, "gcp")
4144
return secret_data
4245
except google.api_core.exceptions.NotFound:
4346
self.logger.error(

src/whispr/logging.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,58 @@
11
"""Logger configuration"""
22

33
import logging
4-
import sys
4+
import os
5+
import platform
6+
from pathlib import Path
7+
from datetime import datetime, timezone
58

69
import structlog
710

811

912
# Configure structlog with human-readable output
13+
def _default_log_path() -> str:
14+
env_path = os.getenv("WHISPR_LOG_PATH")
15+
if env_path:
16+
return env_path
17+
18+
if os.name == "nt":
19+
base_dir = os.getenv("PROGRAMDATA") or os.getenv("LOCALAPPDATA") or str(Path.home())
20+
else:
21+
base_dir = "/var/log"
22+
return str(Path(base_dir) / "whispr" / "access.log")
23+
24+
25+
def _resolve_log_path() -> str:
26+
log_path = _default_log_path()
27+
if _ensure_writable_log_path(log_path):
28+
return log_path
29+
30+
if platform.system() == "Darwin":
31+
fallback_dir = Path.home() / "Library" / "Logs" / "whispr"
32+
else:
33+
fallback_dir = Path.home() / ".local" / "state" / "whispr"
34+
fallback_path = str(fallback_dir / "access.log")
35+
if _ensure_writable_log_path(fallback_path):
36+
return fallback_path
37+
38+
return str(Path.cwd() / "whispr_access.log")
39+
40+
41+
def _ensure_writable_log_path(log_path: str) -> bool:
42+
log_file = Path(log_path)
43+
try:
44+
log_file.parent.mkdir(parents=True, exist_ok=True)
45+
with log_file.open("a", encoding="utf-8"):
46+
pass
47+
return True
48+
except OSError:
49+
return False
50+
51+
1052
def setup_structlog() -> structlog.BoundLogger:
1153
"""Initializes a structured logger"""
54+
log_path = _resolve_log_path()
55+
1256
structlog.configure(
1357
processors=[
1458
structlog.stdlib.filter_by_level,
@@ -27,11 +71,25 @@ def setup_structlog() -> structlog.BoundLogger:
2771
)
2872

2973
# Set up basic configuration for the standard library logging
30-
logging.basicConfig(format="%(message)s", stream=sys.stdout, level=logging.ERROR)
74+
logging.basicConfig(format="%(message)s", handlers=[logging.FileHandler(log_path)], level=logging.INFO)
3175

3276
# Return the structlog logger instance
3377
return structlog.get_logger()
3478

3579

3680
# Initialize logger
3781
logger = setup_structlog()
82+
83+
84+
def log_secret_fetch(
85+
logger_instance: structlog.BoundLogger,
86+
secret_name: str,
87+
vault_type: str,
88+
) -> None:
89+
"""Log a fetched secret with a timezone-aware timestamp."""
90+
logger_instance.info(
91+
"Secret fetched",
92+
secret_name=secret_name,
93+
vault_type=vault_type,
94+
fetched_at=datetime.now(timezone.utc).isoformat(),
95+
)

tests/test_azure.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for Azure module"""
22

33
import unittest
4-
from unittest.mock import Mock, MagicMock
4+
from unittest.mock import Mock, MagicMock, ANY
55

66
import structlog
77
from azure.core.exceptions import ResourceNotFoundError
@@ -37,8 +37,12 @@ def test_fetch_secrets_success(self):
3737

3838
result = self.vault.fetch_secrets("test_secret")
3939
self.assertEqual(result, '{"key": "value"}')
40-
self.mock_logger.info.assert_called_with(
41-
"Successfully fetched secret: test_secret"
40+
self.mock_logger.info.assert_any_call("Successfully fetched secret: test_secret")
41+
self.mock_logger.info.assert_any_call(
42+
"Secret fetched",
43+
secret_name="test_secret",
44+
vault_type="azure",
45+
fetched_at=ANY,
4246
)
4347
self.mock_client.get_secret.assert_called_with("test_secret")
4448

tests/test_gcp.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for GCP module"""
22

33
import unittest
4-
from unittest.mock import MagicMock
4+
from unittest.mock import MagicMock, ANY
55

66
import google.api_core.exceptions
77

@@ -35,9 +35,15 @@ def test_fetch_secrets_success(self):
3535

3636
result = self.vault.fetch_secrets("test_secret")
3737
self.assertEqual(result, '{"key": "value"}')
38-
self.mock_logger.info.assert_called_with(
38+
self.mock_logger.info.assert_any_call(
3939
"Successfully fetched gcp secret: projects/test_project_id/secrets/test_secret/versions/latest"
4040
)
41+
self.mock_logger.info.assert_any_call(
42+
"Secret fetched",
43+
secret_name="projects/test_project_id/secrets/test_secret/versions/latest",
44+
vault_type="gcp",
45+
fetched_at=ANY,
46+
)
4147
self.mock_client.access_secret_version.assert_called_with(
4248
name="projects/test_project_id/secrets/test_secret/versions/latest"
4349
)

0 commit comments

Comments
 (0)