nui-python-shared-utils 1.4.1__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/.github/workflows/ci.yml +11 -7
  2. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/.github/workflows/publish.yml +18 -16
  3. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/.github/workflows/test.yml +4 -2
  4. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/PKG-INFO +1 -1
  5. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/README.md +1 -0
  6. nui_python_shared_utils-1.6.0/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +108 -0
  7. nui_python_shared_utils-1.6.0/docs/decisions/README.md +26 -0
  8. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/powertools-integration.md +32 -16
  9. nui_python_shared_utils-1.6.0/docs/guides/structured-logging-contract.md +251 -0
  10. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_python_shared_utils.egg-info/SOURCES.txt +10 -0
  11. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/__init__.py +39 -0
  12. nui_python_shared_utils-1.6.0/nui_shared_utils/logging/__init__.py +68 -0
  13. nui_python_shared_utils-1.6.0/nui_shared_utils/logging/binding.py +355 -0
  14. nui_python_shared_utils-1.6.0/nui_shared_utils/logging/fields.py +253 -0
  15. nui_python_shared_utils-1.6.0/nui_shared_utils/logging/redaction.py +189 -0
  16. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_client.py +16 -3
  17. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_lazy_imports.py +29 -0
  18. nui_python_shared_utils-1.6.0/tests/test_logging_binding.py +369 -0
  19. nui_python_shared_utils-1.6.0/tests/test_logging_fields.py +150 -0
  20. nui_python_shared_utils-1.6.0/tests/test_redaction.py +127 -0
  21. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_slack_client.py +46 -0
  22. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/.editorconfig +0 -0
  23. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/.markdownlint-cli2.yaml +0 -0
  24. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/AGENTS.md +0 -0
  25. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/CLAUDE.md +0 -0
  26. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/CONTRIBUTING.md +0 -0
  27. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/LICENSE +0 -0
  28. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/MANIFEST.in +0 -0
  29. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/README.md +0 -0
  30. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/development/testing.md +0 -0
  31. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/getting-started/configuration.md +0 -0
  32. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/getting-started/installation.md +0 -0
  33. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/getting-started/quickstart.md +0 -0
  34. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/cli-tools.md +0 -0
  35. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/elasticsearch-integration.md +0 -0
  36. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/jwt-authentication.md +0 -0
  37. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/lambda-utilities.md +0 -0
  38. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/llm-integration.md +0 -0
  39. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/log-processing.md +0 -0
  40. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/shared-types.md +0 -0
  41. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/slack-integration.md +0 -0
  42. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/docs/guides/snowflake-integration.md +0 -0
  43. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/mypy.ini +0 -0
  44. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/__init__.py +0 -0
  45. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/base_client.py +0 -0
  46. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/cli.py +0 -0
  47. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/cloudwatch_metrics.py +0 -0
  48. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/config.py +0 -0
  49. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/db_client.py +0 -0
  50. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/error_handler.py +0 -0
  51. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/es_client.py +0 -0
  52. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/es_query_builder.py +0 -0
  53. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/jwt_auth.py +0 -0
  54. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/lambda_helpers.py +0 -0
  55. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/log_processors.py +0 -0
  56. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/powertools_helpers.py +0 -0
  57. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/secrets_helper.py +0 -0
  58. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_client.py +0 -0
  59. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_formatter.py +0 -0
  60. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/__init__.py +0 -0
  61. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/channel_creator.py +0 -0
  62. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/channel_definitions.py +0 -0
  63. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/setup_helpers.py +0 -0
  64. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/snowflake_client.py +0 -0
  65. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/timezone.py +0 -0
  66. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/utils.py +0 -0
  67. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/base_client.py +0 -0
  68. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/cli.py +0 -0
  69. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/cloudwatch_metrics.py +0 -0
  70. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/config.py +0 -0
  71. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/db_client.py +0 -0
  72. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/error_handler.py +0 -0
  73. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/es_client.py +0 -0
  74. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/es_query_builder.py +0 -0
  75. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/jwt_auth.py +0 -0
  76. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/lambda_helpers.py +0 -0
  77. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/llm.py +0 -0
  78. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/log_processors.py +0 -0
  79. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/powertools_helpers.py +0 -0
  80. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/secrets_helper.py +0 -0
  81. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_formatter.py +0 -0
  82. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/__init__.py +0 -0
  83. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
  84. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
  85. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
  86. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/snowflake_client.py +0 -0
  87. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/timezone.py +0 -0
  88. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/nui_shared_utils/utils.py +0 -0
  89. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/pyproject.toml +0 -0
  90. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/pytest.ini +0 -0
  91. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/redirect/README.md +0 -0
  92. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/redirect/pyproject.toml +0 -0
  93. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/redirect/setup.py +0 -0
  94. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/requirements-test.txt +0 -0
  95. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/scripts/bench_imports.py +0 -0
  96. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/setup.cfg +0 -0
  97. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/setup.py +0 -0
  98. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/__init__.py +0 -0
  99. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_aws_utils.py +0 -0
  100. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_base_client.py +0 -0
  101. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_cloudwatch_metrics.py +0 -0
  102. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_config.py +0 -0
  103. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_db_client.py +0 -0
  104. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_error_handler.py +0 -0
  105. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_es_client.py +0 -0
  106. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_es_query_builder.py +0 -0
  107. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_jwt_auth.py +0 -0
  108. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_lambda_helpers.py +0 -0
  109. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_llm.py +0 -0
  110. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_log_processors.py +0 -0
  111. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_powertools_helpers.py +0 -0
  112. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_secrets_helper.py +0 -0
  113. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_slack_formatter.py +0 -0
  114. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_snowflake_client.py +0 -0
  115. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_timezone.py +0 -0
  116. {nui_python_shared_utils-1.4.1 → nui_python_shared_utils-1.6.0}/tests/test_utils.py +0 -0
@@ -15,15 +15,17 @@ jobs:
15
15
  python-version: ['3.9', '3.10', '3.11', '3.12']
16
16
 
17
17
  steps:
18
- - uses: actions/checkout@v4
18
+ - uses: actions/checkout@v5
19
+ with:
20
+ persist-credentials: false
19
21
 
20
22
  - name: Set up Python ${{ matrix.python-version }}
21
- uses: actions/setup-python@v4
23
+ uses: actions/setup-python@v6
22
24
  with:
23
25
  python-version: ${{ matrix.python-version }}
24
26
 
25
27
  - name: Cache pip dependencies
26
- uses: actions/cache@v3
28
+ uses: actions/cache@v6
27
29
  with:
28
30
  path: ~/.cache/pip
29
31
  key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', '**/setup.py') }}
@@ -65,9 +67,9 @@ jobs:
65
67
  pytest --cov=nui_shared_utils --cov-report=xml --cov-report=term-missing -v
66
68
 
67
69
  - name: Upload coverage to Codecov
68
- uses: codecov/codecov-action@v3
70
+ uses: codecov/codecov-action@v5
69
71
  with:
70
- file: ./coverage.xml
72
+ files: ./coverage.xml
71
73
  flags: unittests
72
74
  name: codecov-umbrella
73
75
  fail_ci_if_error: false
@@ -78,10 +80,12 @@ jobs:
78
80
  needs: test
79
81
 
80
82
  steps:
81
- - uses: actions/checkout@v4
83
+ - uses: actions/checkout@v5
84
+ with:
85
+ persist-credentials: false
82
86
 
83
87
  - name: Set up Python
84
- uses: actions/setup-python@v4
88
+ uses: actions/setup-python@v6
85
89
  with:
86
90
  python-version: '3.11'
87
91
 
@@ -3,9 +3,7 @@ name: Build and Publish to PyPI
3
3
  on:
4
4
  push:
5
5
  tags:
6
- - '[0-9]*' # Trigger on version tags like 1.3.0
7
- release:
8
- types: [published]
6
+ - '[0-9]*' # Trigger on version tags like 1.3.0 (a published GitHub Release also creates the tag)
9
7
  workflow_dispatch: # Allow manual triggering
10
8
 
11
9
  jobs:
@@ -17,10 +15,12 @@ jobs:
17
15
  python-version: ['3.9', '3.10', '3.11', '3.12']
18
16
 
19
17
  steps:
20
- - uses: actions/checkout@v4
21
-
18
+ - uses: actions/checkout@v5
19
+ with:
20
+ persist-credentials: false
21
+
22
22
  - name: Set up Python ${{ matrix.python-version }}
23
- uses: actions/setup-python@v4
23
+ uses: actions/setup-python@v6
24
24
  with:
25
25
  python-version: ${{ matrix.python-version }}
26
26
 
@@ -41,9 +41,9 @@ jobs:
41
41
  pytest --cov=nui_shared_utils --cov-report=xml --cov-report=term-missing
42
42
 
43
43
  - name: Upload coverage to Codecov
44
- uses: codecov/codecov-action@v3
44
+ uses: codecov/codecov-action@v5
45
45
  with:
46
- file: ./coverage.xml
46
+ files: ./coverage.xml
47
47
  fail_ci_if_error: false
48
48
 
49
49
  build:
@@ -52,12 +52,13 @@ jobs:
52
52
  needs: test
53
53
 
54
54
  steps:
55
- - uses: actions/checkout@v4
55
+ - uses: actions/checkout@v5
56
56
  with:
57
+ persist-credentials: false
57
58
  fetch-depth: 0 # Full history for version calculation
58
59
 
59
60
  - name: Set up Python
60
- uses: actions/setup-python@v4
61
+ uses: actions/setup-python@v6
61
62
  with:
62
63
  python-version: '3.11'
63
64
 
@@ -75,7 +76,7 @@ jobs:
75
76
  python -m tarfile -l dist/*.tar.gz
76
77
 
77
78
  - name: Upload artifacts
78
- uses: actions/upload-artifact@v4
79
+ uses: actions/upload-artifact@v6
79
80
  with:
80
81
  name: dist
81
82
  path: dist/
@@ -85,7 +86,7 @@ jobs:
85
86
  runs-on: ubuntu-latest
86
87
  needs: [test, build]
87
88
  # Only publish on tagged releases
88
- if: github.event_name == 'release' || startsWith(github.ref, 'refs/tags/')
89
+ if: startsWith(github.ref, 'refs/tags/')
89
90
  environment:
90
91
  name: pypi
91
92
  url: https://pypi.org/project/nui-python-shared-utils/
@@ -96,7 +97,7 @@ jobs:
96
97
 
97
98
  steps:
98
99
  - name: Download artifacts
99
- uses: actions/download-artifact@v4
100
+ uses: actions/download-artifact@v7
100
101
  with:
101
102
  name: dist
102
103
  path: dist/
@@ -106,13 +107,14 @@ jobs:
106
107
  with:
107
108
  verbose: true
108
109
  print-hash: true
110
+ skip-existing: true # No-op instead of failing if the version is already on PyPI (safe re-runs)
109
111
 
110
112
  publish-test:
111
113
  name: Publish to TestPyPI
112
114
  runs-on: ubuntu-latest
113
115
  needs: [test, build]
114
- # Publish to TestPyPI on manual trigger or development branches
115
- if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/'))
116
+ # Publish to TestPyPI on manual (non-tag) dispatch only; tag pushes go to PyPI above
117
+ if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
116
118
  environment:
117
119
  name: testpypi
118
120
  url: https://test.pypi.org/project/nui-python-shared-utils/
@@ -123,7 +125,7 @@ jobs:
123
125
 
124
126
  steps:
125
127
  - name: Download artifacts
126
- uses: actions/download-artifact@v4
128
+ uses: actions/download-artifact@v7
127
129
  with:
128
130
  name: dist
129
131
  path: dist/
@@ -14,10 +14,12 @@ jobs:
14
14
  python-version: ['3.9', '3.10', '3.11']
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v3
17
+ - uses: actions/checkout@v5
18
+ with:
19
+ persist-credentials: false
18
20
 
19
21
  - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v4
22
+ uses: actions/setup-python@v6
21
23
  with:
22
24
  python-version: ${{ matrix.python-version }}
23
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-python-shared-utils
3
- Version: 1.4.1
3
+ Version: 1.6.0
4
4
  Summary: Shared Python utilities for AWS Lambda, CLI tools, and agents with Slack, Elasticsearch, and monitoring integrations
5
5
  Home-page: https://github.com/nuimarkets/nui-python-shared-utils
6
6
  Author: NUI Markets
@@ -17,6 +17,7 @@ Welcome to the comprehensive documentation for `nui-python-shared-utils` (former
17
17
  Component-specific guides for major features:
18
18
 
19
19
  - **[AWS Powertools Integration](guides/powertools-integration.md)** - Standardized logging, metrics, and error handling
20
+ - **[Structured Logging Contract](guides/structured-logging-contract.md)** - Canonical dotted field registry, request-scoped binders, and payload redaction
20
21
  - **[Lambda Context Helpers](guides/lambda-utilities.md)** - Environment info extraction for logging and metrics
21
22
  - **[Slack Integration](guides/slack-integration.md)** - Messaging, formatting, and file uploads
22
23
  - **[Elasticsearch Integration](guides/elasticsearch-integration.md)** - Search, bulk indexing, health checks
@@ -0,0 +1,108 @@
1
+ # ADR-001: Structured logging field vocabulary
2
+
3
+ **Status**: Proposed
4
+ **Date**: 2026-07-11
5
+
6
+ ## Context
7
+
8
+ This package is the shared logging entry point for a fleet of Python Lambda functions and
9
+ long-running services whose structured logs are shipped to a central log store and queried by
10
+ operators and automated agents. Reliable querying, dashboards, and alerting need a predictable
11
+ field shape: consistent key names and stable leaf types across every producer.
12
+
13
+ Today the logger helper (`get_powertools_logger`) sets service, level, timezone, and timestamp
14
+ format, but takes no position on field names or shape. Each call site hand-assembles its own
15
+ `extra={}` keys. Across the fleet this has grown several incompatible conventions in the same log
16
+ stream: flat single-word keys (`order_id`), dotted namespaced keys (`request.order_id`), nested
17
+ dicts (`extra={"request": {...}}`), and unstructured message strings.
18
+
19
+ This is a **producer-side** problem, and its cost is independent of which store the logs land in:
20
+
21
+ - A **schema-enforcing** store (Elasticsearch) turns field drift into loud failures: mapping
22
+ conflicts and silent nulls when `status` is a number in one document and a string in the next.
23
+ - A **schema-less** store (VictoriaLogs, Loki) accepts the drift silently. `{"status": 200}` and
24
+ `{"status": "timeout"}` are both ingested with no error, and the skew surfaces later as a
25
+ dashboard panel or an alert rule (`5xx rate`, `exception spike`) that quietly stops matching.
26
+
27
+ So the log store is deliberately out of scope for this decision (it is itself under evaluation),
28
+ and the contract is **store-neutral**. The one property of the stack that does constrain the wire
29
+ shape is producer-side:
30
+
31
+ **The Powertools `Logger.append_keys` merge is shallow.** Binding context incrementally with
32
+ nested dicts clobbers earlier keys: `append_keys(request={"a": 1})` then `append_keys(request={"b": 2})`
33
+ emits `request={"b": 2}` and loses `a`. Dotted keys (`append_keys(**{"request.a": 1})`, then
34
+ `request.b`) are independent top-level keys and both survive. (Confirmed on `aws-lambda-powertools`
35
+ 3.6.0.) Incremental binding (bind request at entry, add more later) therefore requires dotted keys.
36
+
37
+ (Incidental, not load-bearing: while Elasticsearch is a store, it expands dotted field names into
38
+ `object`-datatype paths at index time, so `request.order_id` and `{"request": {"order_id": ...}}`
39
+ map and query identically. Wire shape does not change queryability there. A store swap does not
40
+ disturb this decision because the decision does not rest on it.)
41
+
42
+ ## Decision
43
+
44
+ This package owns a canonical **dotted** field vocabulary as the fleet's logging contract, defined
45
+ independently of any log store:
46
+
47
+ - **A field registry (schema object)**: one importable module where each canonical dotted field
48
+ carries its type, whether it is indexed (i.e. meant to be queried vs retrieval-only), a
49
+ sensitivity flag, and any deprecated aliases. Covers `request.*`, `user.*`, `route.*`,
50
+ `response.*`, `error.*`, `trace.*`, `request.body_json` / `response.output_json`, plus a `target`
51
+ routing field. Single source of truth from which the default redaction set and the field-migration
52
+ map are derived. (A plain `dataclass`, not a heavyweight model dependency: the registry is static
53
+ metadata.)
54
+ - **Binding helpers** (`bind_request`, `bind_user`, `log_exception`, and a body helper that
55
+ serialises to a single `request.body_json` string with redaction) so callers never hand-type keys.
56
+ - **A consistency test** that asserts the registry's names, importable so consumers guard against
57
+ drift in CI.
58
+ - The vocabulary is **backend-agnostic**: consumed identically from the Powertools path and from a
59
+ standard-library JSON-formatter path, so short-lived functions and long-running services share one
60
+ contract.
61
+
62
+ Wire shape is **dotted**, names are **snake_case** dotted paths. Nested-dict emission is not used.
63
+
64
+ ## Alternatives considered
65
+
66
+ - **Nested dicts as the emitted shape.** Ergonomic to build in one call and natural in Python.
67
+ Rejected because it clobbers under incremental `append_keys` binding, and because the fleet's
68
+ existing namespaced emitters already use dotted. Nested-*feeling* ergonomics are kept: the binding
69
+ helpers take keyword args and emit dotted keys.
70
+ - **Flat single-word keys** (`order_id`, `user_id`). Simplest, but no namespacing, so unrelated
71
+ fields collide and there is no structural grouping for operators or mappings.
72
+ - **A per-repo constants module.** Two codebases independently grew partial dotted taxonomies, which
73
+ proves the convention is wanted, but a per-repo copy re-introduces drift. One shared registry is
74
+ the point.
75
+ - **Bake the vocabulary into a single logger factory.** Cannot span the two logging backends in use,
76
+ so the contract lives in a backend-agnostic registry plus helpers rather than a factory.
77
+ - **Defer the contract until the log store is chosen.** Rejected: the drift is producer-side and
78
+ costs the same under either candidate store. A store-neutral contract is the piece that survives
79
+ (and is required by) a store migration, not one that blocks on it. Under a schema-less store it is
80
+ the *only* drift guard, since nothing downstream rejects a malformed field.
81
+
82
+ ## Consequences
83
+
84
+ - One field shape across the fleet; the cost of field-name/type drift (loud mapping conflicts on a
85
+ schema-enforcing store, silent query/alert skew on a schema-less one) goes away as consumers adopt
86
+ it.
87
+ - `request.body_json` as one opaque, retrieval-only string keeps unbounded, caller-controlled payloads
88
+ from bloating the field set (a concern under any store, acute on a mapping-based one), and
89
+ centralises redaction in one helper. A response-payload twin (`response.output_json`) applies the
90
+ same treatment where a service logs unbounded response bodies.
91
+ - Callers stop hand-assembling keys; a name change happens in one registry, not N call sites; CI
92
+ catches drift.
93
+ - Consumers must migrate off flat/nested/message-only shapes. Adoption is opt-in per repo and staged,
94
+ so no consumer breaks on adoption.
95
+ - The registry's `type` / `indexed` metadata lets a schema-enforcing store derive its index mapping
96
+ from the registry (define once, derive the mapping). That export is store-specific and belongs with
97
+ the store's own tooling, not in this store-neutral core.
98
+
99
+ ## Load-bearing premises (supersede triggers)
100
+
101
+ - Powertools `append_keys` stays a shallow merge. If it gained deep-merge semantics, the nested
102
+ ergonomics argument would reopen.
103
+ - Request-scoped context (bind at entry, clear at exit) is expressible on both backends without a
104
+ per-request logger factory. If a `contextvars`-based filter cannot isolate concurrent requests
105
+ cleanly on the long-running (non-Lambda) services, the backend-agnostic claim reopens.
106
+ - The value of a producer-side vocabulary does not depend on the store enforcing a schema. (It is
107
+ higher, not lower, when the store enforces nothing.) A change of log store does not supersede this
108
+ ADR.
@@ -0,0 +1,26 @@
1
+ # Architecture Decision Records
2
+
3
+ Decisions that outlive any single change: the "why" behind a choice future readers will want to
4
+ verify, challenge, or build on. These are scoped to this package.
5
+
6
+ ## Format
7
+
8
+ ```markdown
9
+ # ADR-NNN: <Short title>
10
+
11
+ **Status**: Proposed | Accepted | Superseded by ADR-XXX
12
+ **Date**: YYYY-MM-DD
13
+
14
+ ## Context # the situation forcing the decision (present tense, the standing rationale)
15
+ ## Decision # what we decide to do
16
+ ## Alternatives considered
17
+ ## Consequences # what this makes easier, harder, or rules out
18
+ ```
19
+
20
+ Keep each ADR under two pages. Longer than that means it is a plan, not an ADR.
21
+
22
+ ## Index
23
+
24
+ | # | Decision | Status | Date |
25
+ |---|----------|--------|------|
26
+ | [001](ADR-001-structured-logging-field-vocabulary.md) | Structured logging field vocabulary (dotted registry + helpers + consistency test) | Proposed | 2026-07-11 |
@@ -106,20 +106,29 @@ logger = get_powertools_logger("my-service", local_dev_colors=False)
106
106
 
107
107
  ### Structured Logging
108
108
 
109
- The logger supports structured logging with the `extra` parameter:
109
+ The logger supports structured logging with the `extra` parameter. Use the shared
110
+ **[structured logging contract](structured-logging-contract.md)** so field names are
111
+ consistent and queryable across the fleet: dotted, snake_case keys via the `F`
112
+ constants (and consumer-defined domain fields like `order.*`), not ad-hoc flat keys.
110
113
 
111
114
  ```python
115
+ from nui_shared_utils import F
116
+
112
117
  logger.info(
113
118
  "Order processed successfully",
114
119
  extra={
115
- "order_id": "ORD-12345",
116
- "user_id": 789,
117
- "total_amount": 250.00,
118
- "processing_time_ms": 123
119
- }
120
+ "order.id": "ORD-12345", # consumer domain field (order.* namespace)
121
+ F.USER_ID: 789,
122
+ F.RESPONSE_DURATION_MS: 123,
123
+ },
120
124
  )
121
125
  ```
122
126
 
127
+ Better still, bind request/user context once per request with the contract's binding
128
+ helpers so every log line carries it automatically. See the
129
+ [contract guide](structured-logging-contract.md) for `bind_request` / `bind_user` /
130
+ `log_exception` and the full field registry.
131
+
123
132
  ### Elasticsearch-Compatible Timestamps
124
133
 
125
134
  The logger automatically formats timestamps for Elasticsearch compatibility:
@@ -209,7 +218,7 @@ def handler(event, context):
209
218
  ### Complete Example (All Features)
210
219
 
211
220
  ```python
212
- from nui_shared_utils import get_powertools_logger, powertools_handler
221
+ from nui_shared_utils import get_powertools_logger, powertools_handler, F
213
222
 
214
223
  logger = get_powertools_logger("order-processor")
215
224
 
@@ -230,7 +239,7 @@ def handler(event, context):
230
239
  - Proper error response formatting
231
240
  """
232
241
 
233
- logger.info("Processing order", extra={"event_id": event.get("id")})
242
+ logger.info("Processing order", extra={F.REQUEST_ID: event.get("id")})
234
243
 
235
244
  try:
236
245
  order = validate_order(event)
@@ -239,8 +248,8 @@ def handler(event, context):
239
248
  logger.info(
240
249
  "Order processed successfully",
241
250
  extra={
242
- "order_id": order["id"],
243
- "processing_time_ms": result["duration"]
251
+ "order.id": order["id"], # consumer domain field
252
+ F.RESPONSE_DURATION_MS: result["duration"],
244
253
  }
245
254
  )
246
255
 
@@ -250,7 +259,7 @@ def handler(event, context):
250
259
  }
251
260
 
252
261
  except ValidationError as e:
253
- logger.error("Order validation failed", extra={"error": str(e)})
262
+ logger.error("Order validation failed", extra={F.ERROR_TYPE: type(e).__name__, F.ERROR_MESSAGE: str(e)})
254
263
  return {
255
264
  "statusCode": 400,
256
265
  "body": json.dumps({"error": "Invalid order"})
@@ -510,12 +519,17 @@ logger: Union[PowertoolsLogger, PythonLogger] = get_powertools_logger("my-servic
510
519
 
511
520
  ### 1. Use Structured Logging
512
521
 
522
+ Use the [structured logging contract](structured-logging-contract.md)'s dotted field
523
+ names (via `F` and consumer domain fields), not ad-hoc flat keys:
524
+
513
525
  ```python
514
- # ✓ Good - Searchable, filterable
526
+ from nui_shared_utils import F
527
+
528
+ # ✓ Good - Searchable, filterable, consistent field names
515
529
  logger.info("Order created", extra={
516
- "order_id": order.id,
517
- "user_id": user.id,
518
- "amount": order.total
530
+ "order.id": order.id, # consumer domain field
531
+ F.USER_ID: user.id,
532
+ F.RESPONSE_STATUS: 200,
519
533
  })
520
534
 
521
535
  # ✗ Bad - Unstructured string
@@ -563,6 +577,8 @@ def handler(event, context):
563
577
  ### 4. Graceful Error Handling
564
578
 
565
579
  ```python
580
+ from nui_shared_utils import F
581
+
566
582
  @powertools_handler(service_name="my-service", slack_alert_channel="#errors")
567
583
  @logger.inject_lambda_context
568
584
  def handler(event, context):
@@ -573,7 +589,7 @@ def handler(event, context):
573
589
 
574
590
  except ValidationError as e:
575
591
  # Expected errors - log and return 400
576
- logger.warning("Validation failed", extra={"error": str(e)})
592
+ logger.warning("Validation failed", extra={F.ERROR_TYPE: type(e).__name__, F.ERROR_MESSAGE: str(e)})
577
593
  return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
578
594
 
579
595
  # Unexpected errors caught by decorator -> 500 + Slack alert