Skip to content

Commit 894d166

Browse files
rads-1996Copilothectorhdzg
authored
Add support for multiuser permissions in unix (#43483)
* Add support for multiuser permissions in unix * Updated CHANGELOG * Update sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_storage.py Co-authored-by: Copilot <[email protected]> * Update sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_storage.py Co-authored-by: Copilot <[email protected]> * Fix lint * Remove unused input * Revert storage changes * Fix path formation to allow multiple users unique paths to access * Fixed path and added unique identifier right after \tmp * fix cspell * remove commented line * Retrigger CI/CD pipeline * Retrigger CI/CD pipeline * Retrigger CI/CD pipeline * Add docstring and exception handling * Update sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md Co-authored-by: Hector Hernandez <[email protected]> * Update sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py Co-authored-by: Hector Hernandez <[email protected]> * Cleanup * Correct keyword * Docstring * Modified Path Logic * Retrigger CI/CD pipeline * Fix lint * Retrigger CI/CD pipeline --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Hector Hernandez <[email protected]>
1 parent e4d4839 commit 894d166

File tree

5 files changed

+265
-12
lines changed

5 files changed

+265
-12
lines changed

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## 1.0.0b45 (Unreleased)
44

55
### Features Added
6+
- Added local storage support for multiple users on the same Linux system
7+
([#43483](https://github.com/Azure/azure-sdk-for-python/pull/43483))
68

79
### Breaking Changes
810

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import threading
1010
import time
1111
import warnings
12+
import hashlib
1213
from typing import Callable, Dict, Any, Optional
1314

1415
from opentelemetry.semconv.resource import ResourceAttributes
@@ -422,3 +423,6 @@ def get_compute_type():
422423
if _is_on_aks():
423424
return _RP_Names.AKS.value
424425
return _RP_Names.UNKNOWN.value
426+
427+
def _get_sha256_hash(input_str: str) -> str:
428+
return hashlib.sha256(input_str.encode("utf-8")).hexdigest()

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3+
import getpass
34
import logging
45
import os
56
import tempfile
67
import time
8+
import sys
9+
from pathlib import Path
710
from enum import Enum
811
from typing import List, Optional, Any
912
from urllib.parse import urlparse
13+
import psutil
1014

1115
from azure.core.exceptions import HttpResponseError, ServiceRequestError
1216
from azure.core.pipeline.policies import (
@@ -54,6 +58,7 @@
5458
_get_attach_type,
5559
_get_rp,
5660
ext_version,
61+
_get_sha256_hash
5762
)
5863
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
5964
get_statsbeat_initial_success,
@@ -78,7 +83,7 @@
7883

7984
logger = logging.getLogger(__name__)
8085

81-
_AZURE_TEMPDIR_PREFIX = "Microsoft/AzureMonitor"
86+
_AZURE_TEMPDIR_PREFIX = "Microsoft-AzureMonitor-"
8287
_TEMPDIR_PREFIX = "opentelemetry-python-"
8388
_SERVICE_API_LATEST = "2020-09-15_Preview"
8489

@@ -133,13 +138,10 @@ def __init__(self, **kwargs: Any) -> None:
133138
self._storage_min_retry_interval = kwargs.get(
134139
"storage_min_retry_interval", 60
135140
) # minimum retry interval in seconds
136-
temp_suffix = self._instrumentation_key or ""
137141
if "storage_directory" in kwargs:
138142
self._storage_directory = kwargs.get("storage_directory")
139143
elif not self._disable_offline_storage:
140-
self._storage_directory = os.path.join(
141-
tempfile.gettempdir(), _AZURE_TEMPDIR_PREFIX, _TEMPDIR_PREFIX + temp_suffix
142-
)
144+
self._storage_directory = _get_storage_directory(self._instrumentation_key or "")
143145
else:
144146
self._storage_directory = None
145147
self._storage_retention_period = kwargs.get(
@@ -604,3 +606,61 @@ def _get_authentication_credential(**kwargs: Any) -> Optional[ManagedIdentityCre
604606
except Exception as e:
605607
logger.error("Failed to get authentication credential and enable AAD: %s", e) # pylint: disable=do-not-log-exceptions-if-not-debug
606608
return None
609+
610+
def _get_storage_directory(instrumentation_key: str) -> str:
611+
"""Return the deterministic local storage path for a given instrumentation key.
612+
613+
On shared Linux hosts the first user to create ``/tmp/Microsoft/AzureMonitor`` can
614+
block others because the directory inherits that user's ``umask``. This is avoided by
615+
inserting a hash of the instrumentation key, user name, process name, and
616+
application directory, giving each user their own subdirectory, e.g.
617+
``/tmp/Microsoft-AzureMonitor-1234...../opentelemetry-python-<ikey>``.
618+
619+
:param str instrumentation_key: Application Insights instrumentation key.
620+
:return: Absolute path to the storage directory.
621+
:rtype: str
622+
"""
623+
624+
def _safe_psutil_call(func, default=""):
625+
try:
626+
return func() or default
627+
except psutil.Error:
628+
return default
629+
630+
shared_root = tempfile.gettempdir()
631+
632+
process = None
633+
try:
634+
process = psutil.Process()
635+
except psutil.Error:
636+
pass
637+
638+
process_name = _safe_psutil_call(process.name) if process else ""
639+
candidate_path = _safe_psutil_call(process.cwd) if process else ""
640+
641+
if not candidate_path:
642+
candidate_path = sys.argv[0] if sys.argv else ""
643+
644+
try:
645+
application_directory = Path(candidate_path or ".").resolve()
646+
except Exception:
647+
application_directory = Path(shared_root)
648+
649+
try:
650+
user_segment = getpass.getuser()
651+
except Exception:
652+
user_segment = ""
653+
654+
hash_input = ";".join(
655+
[
656+
instrumentation_key,
657+
user_segment,
658+
process_name,
659+
os.fspath(application_directory), # cspell:disable-line
660+
]
661+
)
662+
subdirectory = _get_sha256_hash(hash_input)
663+
storage_directory = os.path.join(
664+
shared_root, _AZURE_TEMPDIR_PREFIX + subdirectory, _TEMPDIR_PREFIX + instrumentation_key
665+
)
666+
return storage_directory

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_base_exporter.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
_get_authentication_credential,
1818
BaseExporter,
1919
ExportResult,
20+
_get_storage_directory,
2021
)
2122
from azure.monitor.opentelemetry.exporter._storage import StorageExportResult
2223
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
@@ -55,6 +56,7 @@
5556

5657
TEST_AUTH_POLICY = "TEST_AUTH_POLICY"
5758
TEST_TEMP_DIR = "TEST_TEMP_DIR"
59+
TEST_USER_DIR = "test-user"
5860

5961

6062
def throw(exc_type, *args, **kwargs):
@@ -174,15 +176,84 @@ def test_constructor_no_storage_directory(self, mock_get_temp_dir):
174176
self.assertEqual(base._timeout, 10)
175177
self.assertEqual(base._api_version, "2021-02-10_Preview")
176178
self.assertEqual(base._storage_min_retry_interval, 100)
179+
storage_directory = _get_storage_directory(instrumentation_key="4321abcd-5678-4efa-8abc-1234567890ab")
177180
self.assertEqual(
178181
base._storage_directory,
179-
os.path.join(
180-
TEST_TEMP_DIR,
181-
"Microsoft/AzureMonitor",
182-
"opentelemetry-python-" + "4321abcd-5678-4efa-8abc-1234567890ab",
183-
),
182+
storage_directory
184183
)
185-
mock_get_temp_dir.assert_called_once()
184+
185+
@mock.patch("azure.monitor.opentelemetry.exporter.export._base.tempfile.gettempdir")
186+
def test_constructor_no_storage_directory_and_invalid_instrumentation_key(self, mock_get_temp_dir):
187+
mock_get_temp_dir.return_value = TEST_TEMP_DIR
188+
base = BaseExporter(
189+
api_version="2021-02-10_Preview",
190+
connection_string="InstrumentationKey=4321abcd-5678-4efa-8abc-1234567890ab;IngestionEndpoint=https://westus-0.in.applicationinsights.azure.com/",
191+
disable_offline_storage=False,
192+
distro_version="1.0.0",
193+
storage_maintenance_period=30,
194+
storage_max_size=1000,
195+
storage_min_retry_interval=100,
196+
storage_retention_period=2000,
197+
)
198+
self.assertEqual(
199+
base._instrumentation_key,
200+
"4321abcd-5678-4efa-8abc-1234567890ab",
201+
)
202+
self.assertEqual(
203+
base._endpoint,
204+
"https://westus-0.in.applicationinsights.azure.com/",
205+
)
206+
self.assertEqual(base._distro_version, "1.0.0")
207+
self.assertIsNotNone(base.storage)
208+
self.assertEqual(base.storage._max_size, 1000)
209+
self.assertEqual(base.storage._retention_period, 2000)
210+
self.assertEqual(base._storage_maintenance_period, 30)
211+
self.assertEqual(base._timeout, 10)
212+
self.assertEqual(base._api_version, "2021-02-10_Preview")
213+
self.assertEqual(base._storage_min_retry_interval, 100)
214+
storage_directory = _get_storage_directory(instrumentation_key="")
215+
self.assertNotEqual(
216+
base._storage_directory,
217+
storage_directory
218+
)
219+
220+
@mock.patch("azure.monitor.opentelemetry.exporter.export._base.getpass.getuser")
221+
@mock.patch("azure.monitor.opentelemetry.exporter.export._base.tempfile.gettempdir")
222+
def test_constructor_no_storage_directory_and_invalid_user_details(self, mock_get_temp_dir, mock_get_user):
223+
mock_get_temp_dir.return_value = TEST_TEMP_DIR
224+
mock_get_user.side_effect = OSError("failed to resolve user")
225+
base = BaseExporter(
226+
api_version="2021-02-10_Preview",
227+
connection_string="InstrumentationKey=4321abcd-5678-4efa-8abc-1234567890ab;IngestionEndpoint=https://westus-0.in.applicationinsights.azure.com/",
228+
disable_offline_storage=False,
229+
distro_version="1.0.0",
230+
storage_maintenance_period=30,
231+
storage_max_size=1000,
232+
storage_min_retry_interval=100,
233+
storage_retention_period=2000,
234+
)
235+
self.assertEqual(
236+
base._instrumentation_key,
237+
"4321abcd-5678-4efa-8abc-1234567890ab",
238+
)
239+
self.assertEqual(
240+
base._endpoint,
241+
"https://westus-0.in.applicationinsights.azure.com/",
242+
)
243+
self.assertEqual(base._distro_version, "1.0.0")
244+
self.assertIsNotNone(base.storage)
245+
self.assertEqual(base.storage._max_size, 1000)
246+
self.assertEqual(base.storage._retention_period, 2000)
247+
self.assertEqual(base._storage_maintenance_period, 30)
248+
self.assertEqual(base._timeout, 10)
249+
self.assertEqual(base._api_version, "2021-02-10_Preview")
250+
self.assertEqual(base._storage_min_retry_interval, 100)
251+
storage_directory = _get_storage_directory(instrumentation_key="4321abcd-5678-4efa-8abc-1234567890ab")
252+
self.assertEqual(
253+
base._storage_directory,
254+
storage_directory
255+
)
256+
mock_get_user.assert_called()
186257

187258
@mock.patch("azure.monitor.opentelemetry.exporter.export._base.tempfile.gettempdir")
188259
def test_constructor_disable_offline_storage(self, mock_get_temp_dir):

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_storage.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import os
55
import shutil
6+
import tempfile
67
import unittest
78
from unittest import mock
89

@@ -14,7 +15,12 @@
1415
_seconds,
1516
)
1617

18+
from azure.monitor.opentelemetry.exporter.export._base import _get_storage_directory
19+
1720
TEST_FOLDER = os.path.abspath(".test.storage")
21+
DUMMY_INSTRUMENTATION_KEY = "00000000-0000-0000-0000-000000000000"
22+
TEST_USER = "multiuser-test"
23+
STORAGE_MODULE = "azure.monitor.opentelemetry.exporter._storage"
1824

1925

2026
def throw(exc_type, *args, **kwargs):
@@ -507,7 +513,7 @@ def test_check_and_set_folder_permissions_readonly_filesystem_sets_readonly_stat
507513

508514
# Clean up - note: cannot easily reset readonly state, but test isolation should handle this
509515
set_local_storage_setup_state_exception("")
510-
516+
511517
def test_check_and_set_folder_permissions_windows_icacls_failure_sets_exception_state(self):
512518
test_input = (1, 2, 3)
513519

@@ -907,3 +913,113 @@ def test_readonly_state_idempotent_set_operations(self):
907913
# State should remain True after multiple sets
908914
final_state = get_local_storage_setup_state_readonly()
909915
self.assertTrue(final_state)
916+
917+
def test_check_and_set_folder_permissions_unix_multiuser_scenario(self):
918+
919+
from azure.monitor.opentelemetry.exporter.statsbeat.customer._state import (
920+
get_local_storage_setup_state_exception,
921+
set_local_storage_setup_state_exception,
922+
)
923+
924+
# Clear any existing exception state
925+
set_local_storage_setup_state_exception("")
926+
927+
storage_abs_path = _get_storage_directory(DUMMY_INSTRUMENTATION_KEY)
928+
929+
with mock.patch(f"{STORAGE_MODULE}.os.name", "posix"):
930+
chmod_calls = []
931+
makedirs_calls = []
932+
933+
def mock_chmod(path, mode):
934+
chmod_calls.append((path, oct(mode)))
935+
936+
def mock_makedirs(path, mode=0o777, exist_ok=False):
937+
makedirs_calls.append((path, oct(mode), exist_ok))
938+
939+
with mock.patch(f"{STORAGE_MODULE}.os.makedirs", side_effect=mock_makedirs):
940+
with mock.patch(f"{STORAGE_MODULE}.os.chmod", side_effect=mock_chmod):
941+
with mock.patch(f"{STORAGE_MODULE}.os.path.abspath", side_effect=lambda path: path):
942+
stor = LocalFileStorage(storage_abs_path)
943+
944+
self.assertTrue(stor._enabled)
945+
946+
self.assertEqual(
947+
makedirs_calls,
948+
[(storage_abs_path, '0o777', True)],
949+
f"Unexpected makedirs calls: {makedirs_calls}",
950+
)
951+
952+
self.assertEqual(
953+
{(storage_abs_path, '0o700')},
954+
{(call_path, mode) for call_path, mode in chmod_calls},
955+
f"Unexpected chmod calls: {chmod_calls}",
956+
)
957+
958+
stor.close()
959+
960+
# Clean up
961+
set_local_storage_setup_state_exception("")
962+
963+
def test_check_and_set_folder_permissions_unix_multiuser_parent_permission_failure(self):
964+
from azure.monitor.opentelemetry.exporter.statsbeat.customer._state import (
965+
get_local_storage_setup_state_exception,
966+
set_local_storage_setup_state_exception,
967+
)
968+
969+
# Clear any existing exception state
970+
set_local_storage_setup_state_exception("")
971+
972+
storage_abs_path = _get_storage_directory(DUMMY_INSTRUMENTATION_KEY)
973+
974+
with mock.patch(f"{STORAGE_MODULE}.os.name", "posix"):
975+
def mock_makedirs(path, mode=0o777, exist_ok=False):
976+
raise PermissionError("Operation not permitted on parent directory")
977+
978+
with mock.patch(f"{STORAGE_MODULE}.os.makedirs", side_effect=mock_makedirs):
979+
with mock.patch(f"{STORAGE_MODULE}.os.chmod"):
980+
with mock.patch(f"{STORAGE_MODULE}.os.path.abspath", side_effect=lambda path: path):
981+
stor = LocalFileStorage(storage_abs_path)
982+
983+
self.assertFalse(stor._enabled)
984+
985+
exception_state = get_local_storage_setup_state_exception()
986+
self.assertEqual(exception_state, "Operation not permitted on parent directory")
987+
988+
stor.close()
989+
990+
# Clean up
991+
set_local_storage_setup_state_exception("")
992+
993+
def test_check_and_set_folder_permissions_unix_multiuser_storage_permission_failure(self):
994+
test_error_message = "PermissionError: Operation not permitted on storage directory"
995+
996+
from azure.monitor.opentelemetry.exporter.statsbeat.customer._state import (
997+
get_local_storage_setup_state_exception,
998+
set_local_storage_setup_state_exception,
999+
)
1000+
1001+
# Clear any existing exception state
1002+
set_local_storage_setup_state_exception("")
1003+
1004+
storage_abs_path = _get_storage_directory(DUMMY_INSTRUMENTATION_KEY)
1005+
1006+
with mock.patch(f"{STORAGE_MODULE}.os.name", "posix"):
1007+
def mock_chmod(path, mode):
1008+
if mode == 0o700:
1009+
raise PermissionError(test_error_message)
1010+
raise OSError(f"Unexpected chmod call: {path}, {oct(mode)}")
1011+
1012+
with mock.patch(f"{STORAGE_MODULE}.os.makedirs"):
1013+
with mock.patch(f"{STORAGE_MODULE}.os.chmod", side_effect=mock_chmod):
1014+
with mock.patch(f"{STORAGE_MODULE}.os.path.abspath", side_effect=lambda path: path):
1015+
stor = LocalFileStorage(storage_abs_path)
1016+
1017+
self.assertFalse(stor._enabled)
1018+
1019+
exception_state = get_local_storage_setup_state_exception()
1020+
self.assertEqual(exception_state, test_error_message)
1021+
1022+
stor.close()
1023+
1024+
# Clean up
1025+
set_local_storage_setup_state_exception("")

0 commit comments

Comments
 (0)