5 Commits

Author SHA1 Message Date
Eduardo Rios
279484b2aa fix(logs): do not log the MongoDB credentials
DataManager logged the full connection string, user and password included,
at startup. Log it through redact_uri (mongodb://***@host:port/...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:06:18 -03:00
Eduardo Rios
ce67105c16 fix(deps): sientia_do 1.12.5 so the Postgres engine works with SQLAlchemy 2.1
SQLAlchemy 2.1 makes psycopg (v3) the default driver for postgresql:// URLs.
sientia_do 1.12.2 builds that URL and only psycopg2-binary is installed, so a
fresh install (any pod restart) fails with "No module named 'psycopg'" and
the gateway runtime does not start (no NATS write consumer). 1.12.5 uses
postgresql+psycopg2:// explicitly (sientia-dataops-library c402a75).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:06:17 -03:00
github-actions[bot]
d83917a30f chore: bump version to 2.2.0-rc1 2026-10-13 16:59:56 +00:00
54c854bb0a Merge pull request #42 from Aignosi/feature-rc/SIENTIAPDE-1974-release-slot-keyerror
SIENTIAPDE-1974: fix gateway main loop crash (KeyError) on slot unsubscribe
2026-10-13 13:59:26 -03:00
Eduardo Rios
06caa88ed8 SIENTIAPDE-1974: chore(slots): drop NOSONAR, accept S7504 in SonarQube instead
Review (Vitor): avoid NOSONAR; when a SonarQube issue does not apply, accept it
in SonarQube so the decision is recorded there. The list() snapshot of the
readers stays (the loop awaits) and the comment says why S7504 is accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:24:54 -03:00
5 changed files with 72 additions and 10 deletions

View File

@@ -12,6 +12,22 @@ from sientia_do.temporal.constants import now
import ingestor.metrics as metrics
def redact_uri(uri: str) -> str:
"""
Returns ``uri`` without the user and password, safe to log
(``mongodb://user:pass@host:27017/db`` -> ``mongodb://***@host:27017/db``).
"""
prefix, sep, rest = uri.partition('://')
if not sep:
prefix, rest = '', uri
ends = [i for i in (rest.find('/'), rest.find('?')) if i != -1]
end = min(ends) if ends else len(rest)
authority, tail = rest[:end], rest[end:]
if '@' not in authority:
return uri
return f'{prefix}{sep}***@{authority.rpartition("@")[2]}{tail}'
class DataManager(SientiaMonitoring):
"""
Manages data persistence operations for the OPC Ingestor.
@@ -66,7 +82,8 @@ class DataManager(SientiaMonitoring):
)
logger.info(
f'Trying to initializing DataManager with MongoDB servers: {mongo_connection_string}'
'Trying to initializing DataManager with MongoDB servers: '
f'{redact_uri(mongo_connection_string)}'
)
self.connection_string = mongo_connection_string
@@ -82,7 +99,9 @@ class DataManager(SientiaMonitoring):
self.metadata = metadata
logger.info(f'DataManager initialized with MongoDB servers: {self.connection_string}')
logger.info(
f'DataManager initialized with MongoDB servers: {redact_uri(self.connection_string)}'
)
def shutdown(self):
"""

View File

@@ -561,8 +561,9 @@ class IngestorManager(SientiaMonitoring):
"""
# list() snapshots the readers: the loop awaits, so a reader added or removed
# meanwhile must not break the iteration.
for server, reader in list(self.opc_managers.items()): # NOSONAR
# meanwhile must not break the iteration. SonarQube flags it as an unnecessary
# list() (S7504); the issue is accepted in SonarQube for this reason.
for server, reader in list(self.opc_managers.items()):
if slot in reader.subscriptions:
await self._unsubscribe_reader(server, reader, slot)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ingestor"
version = "2.1.0-rc1"
version = "2.2.0-rc1"
description = "Sientia DataOps Ingestor - OPC Tag Ingestor"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -1,9 +1,8 @@
asyncua
redis
# TODO(SIENTIAPDE-1972): bump once sientia-dataops-library releases SIENTIAPDE-1590
# (generic Mongo find_one/distinct/aggregate methods) — not in 1.12.2 yet.
# Local dev uses an editable install of the local checkout (feature/SIENTIAPDE-1590).
sientia_do==1.12.2
# 1.12.5 builds the Postgres engine with an explicit postgresql+psycopg2:// URL: SQLAlchemy 2.1
# defaults postgresql:// to psycopg (v3), which is not installed ("No module named 'psycopg'").
sientia_do==1.12.5
prometheus_client
pymongo
nats-py>=2.6

View File

@@ -1,9 +1,10 @@
from unittest.mock import ANY, AsyncMock, MagicMock, patch
import pytest
from pytest import fixture, mark
from sientia_do.notifications.models import NotificationLevel
from ingestor.managers.data_manager import DataManager
from ingestor.managers.data_manager import DataManager, redact_uri
metadata = {
'metadata': {
@@ -54,6 +55,48 @@ def test___init___success(mongodb_repository):
assert data_manager.connection_string == 'mongodb://localhost:27017'
@patch('ingestor.managers.data_manager.MongoDBRepository')
def test___init___does_not_log_the_credentials(mongodb_repository):
logger_mock = MagicMock()
uri = 'mongodb://root:s3cr3t@mongodb.mongodb.svc.cluster.local:27017'
data_manager = DataManager(
metadata=metadata['metadata'],
mongo_connection_string=uri,
mongo_database='sientia',
logger=logger_mock,
notification_handler=MagicMock(),
metrics_controller=MagicMock(),
)
logged = ' '.join(str(c) for c in logger_mock.info.call_args_list)
assert 's3cr3t' not in logged
assert 'root' not in logged
logger_mock.info.assert_any_call(
'DataManager initialized with MongoDB servers: '
'mongodb://***@mongodb.mongodb.svc.cluster.local:27017'
)
assert data_manager.connection_string == uri
@pytest.mark.parametrize(
('uri', 'expected'),
[
('mongodb://localhost:27017', 'mongodb://localhost:27017'),
(
'mongodb://u:p@h1:27017,h2:27017/db?authSource=admin',
'mongodb://***@h1:27017,h2:27017/db?authSource=admin',
),
('mongodb://u:p@h:27017?authSource=admin', 'mongodb://***@h:27017?authSource=admin'),
('mongodb+srv://u@cluster.example.com', 'mongodb+srv://***@cluster.example.com'),
('u:p@h:27017', '***@h:27017'),
('mongodb://h:27017/db?x=a@b', 'mongodb://h:27017/db?x=a@b'),
],
)
def test_redact_uri(uri, expected):
assert redact_uri(uri) == expected
def test_shutdown(data_manager):
close_mock = MagicMock()
data_manager.mongo_repository.close = close_mock