Skip to content

Commit 6e529c8

Browse files
hectorhdzgCopilot
andauthored
[Monitor OpenTelemetry Exporter] Add folder permissions for local files (#41384)
* WIP * Update * Update changelog * Update sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_storage.py Co-authored-by: Copilot <[email protected]> * Update * Lint * Update * Log exception * Update * Lint * Test * test * Test * Remove env clear in tests * Update --------- Co-authored-by: Copilot <[email protected]>
1 parent d66b516 commit 6e529c8

File tree

11 files changed

+133
-60
lines changed

11 files changed

+133
-60
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636

3737
- Extend version range for `psutil` to include 7.x
3838
([#40459](https://github.com/Azure/azure-sdk-for-python/pull/40459))
39+
- Add folder permissions for local files
40+
([#41384](https://github.com/Azure/azure-sdk-for-python/pull/41384))
3941

4042
## 1.0.0b36 (2025-04-07)
4143

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

Lines changed: 111 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
import logging
77
import os
88
import random
9+
import subprocess
910

1011
from azure.monitor.opentelemetry.exporter._utils import PeriodicTask
1112

1213
logger = logging.getLogger(__name__)
1314

15+
ICACLS_PATH = os.path.join(
16+
os.environ.get("SYSTEMDRIVE", "C:"), r"\Windows\System32\icacls.exe"
17+
)
1418

1519
def _fmt(timestamp):
1620
return timestamp.strftime("%Y-%m-%dT%H%M%S.%f")
@@ -92,19 +96,25 @@ def __init__(
9296
self._max_size = max_size
9397
self._retention_period = retention_period
9498
self._write_timeout = write_timeout
95-
self._maintenance_routine()
96-
self._maintenance_task = PeriodicTask(
97-
interval=maintenance_period,
98-
function=self._maintenance_routine,
99-
name=name,
100-
)
101-
self._lease_period = lease_period
102-
self._maintenance_task.daemon = True
103-
self._maintenance_task.start()
99+
100+
self._enabled = self._check_and_set_folder_permissions()
101+
if self._enabled:
102+
self._maintenance_routine()
103+
self._maintenance_task = PeriodicTask(
104+
interval=maintenance_period,
105+
function=self._maintenance_routine,
106+
name=name,
107+
)
108+
self._lease_period = lease_period
109+
self._maintenance_task.daemon = True
110+
self._maintenance_task.start()
111+
else:
112+
logger.error("Could not set secure permissions on storage folder, local storage is disabled.")
104113

105114
def close(self):
106-
self._maintenance_task.cancel()
107-
self._maintenance_task.join()
115+
if self._enabled:
116+
self._maintenance_task.cancel()
117+
self._maintenance_task.join()
108118

109119
def __enter__(self):
110120
return self
@@ -121,43 +131,49 @@ def _maintenance_routine(self):
121131
except Exception:
122132
pass # keep silent
123133

134+
# pylint: disable=too-many-nested-blocks
124135
def gets(self):
125-
now = _now()
126-
lease_deadline = _fmt(now)
127-
retention_deadline = _fmt(now - _seconds(self._retention_period))
128-
timeout_deadline = _fmt(now - _seconds(self._write_timeout))
129-
try:
130-
for name in sorted(os.listdir(self._path)):
131-
path = os.path.join(self._path, name)
132-
if not os.path.isfile(path):
133-
continue # skip if not a file
134-
if path.endswith(".tmp"):
135-
if name < timeout_deadline:
136+
if self._enabled:
137+
now = _now()
138+
lease_deadline = _fmt(now)
139+
retention_deadline = _fmt(now - _seconds(self._retention_period))
140+
timeout_deadline = _fmt(now - _seconds(self._write_timeout))
141+
try:
142+
for name in sorted(os.listdir(self._path)):
143+
path = os.path.join(self._path, name)
144+
if not os.path.isfile(path):
145+
continue # skip if not a file
146+
if path.endswith(".tmp"):
147+
if name < timeout_deadline:
148+
try:
149+
os.remove(path) # TODO: log data loss
150+
except Exception:
151+
pass # keep silent
152+
if path.endswith(".lock"):
153+
if path[path.rindex("@") + 1 : -5] > lease_deadline:
154+
continue # under lease
155+
new_path = path[: path.rindex("@")]
136156
try:
137-
os.remove(path) # TODO: log data loss
157+
os.rename(path, new_path)
138158
except Exception:
139159
pass # keep silent
140-
if path.endswith(".lock"):
141-
if path[path.rindex("@") + 1 : -5] > lease_deadline:
142-
continue # under lease
143-
new_path = path[: path.rindex("@")]
144-
try:
145-
os.rename(path, new_path)
146-
except Exception:
147-
pass # keep silent
148-
path = new_path
149-
if path.endswith(".blob"):
150-
if name < retention_deadline:
151-
try:
152-
os.remove(path) # TODO: log data loss
153-
except Exception:
154-
pass # keep silent
155-
else:
156-
yield LocalFileBlob(path)
157-
except Exception:
158-
pass # keep silent
160+
path = new_path
161+
if path.endswith(".blob"):
162+
if name < retention_deadline:
163+
try:
164+
os.remove(path) # TODO: log data loss
165+
except Exception:
166+
pass # keep silent
167+
else:
168+
yield LocalFileBlob(path)
169+
except Exception:
170+
pass # keep silent
171+
else:
172+
pass
159173

160174
def get(self):
175+
if not self._enabled:
176+
return None
161177
cursor = self.gets()
162178
try:
163179
return next(cursor)
@@ -166,12 +182,8 @@ def get(self):
166182
return None
167183

168184
def put(self, data, lease_period=None):
169-
# Create path if it doesn't exist
170-
try:
171-
if not os.path.isdir(self._path):
172-
os.makedirs(self._path, exist_ok=True)
173-
except Exception:
174-
pass # keep silent
185+
if not self._enabled:
186+
return None
175187
if not self._check_storage_size():
176188
return None
177189
blob = LocalFileBlob(
@@ -187,6 +199,44 @@ def put(self, data, lease_period=None):
187199
lease_period = self._lease_period
188200
return blob.put(data, lease_period=lease_period)
189201

202+
def _check_and_set_folder_permissions(self):
203+
"""
204+
Validate and set folder permissions where the telemetry data will be stored.
205+
:return: True if folder was created and permissions set successfully, False otherwise.
206+
:rtype: bool
207+
"""
208+
try:
209+
# Create path if it doesn't exist
210+
os.makedirs(self._path, exist_ok=True)
211+
# Windows
212+
if os.name == "nt":
213+
user = self._get_current_user()
214+
if not user:
215+
logger.warning(
216+
"Failed to retrieve current user. Skipping folder permission setup."
217+
)
218+
return False
219+
result = subprocess.run(
220+
[
221+
ICACLS_PATH,
222+
self._path,
223+
"/grant",
224+
"*S-1-5-32-544:(OI)(CI)F", # Full permission for Administrators
225+
f"{user}:(OI)(CI)F",
226+
"/inheritance:r",
227+
],
228+
check=False,
229+
)
230+
if result.returncode == 0:
231+
return True
232+
# Unix
233+
else:
234+
os.chmod(self._path, 0o700)
235+
return True
236+
except Exception:
237+
pass # keep silent
238+
return False
239+
190240
def _check_storage_size(self):
191241
size = 0
192242
# pylint: disable=unused-variable
@@ -209,7 +259,19 @@ def _check_storage_size(self):
209259
"Persistent storage max capacity has been "
210260
"reached. Currently at {}KB. Telemetry will be "
211261
"lost. Please consider increasing the value of "
212-
"'storage_max_size' in exporter config.".format(str(size / 1024))
262+
"'storage_max_size' in exporter config.".format(
263+
str(size / 1024)
264+
)
213265
)
214266
return False
215267
return True
268+
269+
def _get_current_user(self):
270+
user = ""
271+
domain = os.environ.get("USERDOMAIN")
272+
username = os.environ.get("USERNAME")
273+
if domain and username:
274+
user = f"{domain}\\{username}"
275+
else:
276+
user = os.getlogin()
277+
return user

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/logs/test_logs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class TestAzureLogExporter(unittest.TestCase):
5757

5858
@classmethod
5959
def setUpClass(cls):
60-
os.environ.clear()
60+
os.environ.pop("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL", None)
61+
os.environ.pop("APPINSIGHTS_INSTRUMENTATIONKEY", None)
6162
os.environ["APPINSIGHTS_INSTRUMENTATIONKEY"] = "1234abcd-5678-4efa-8abc-1234567890ab"
6263
os.environ["APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"] = "true"
6364
cls._exporter = cls._exporter_class()

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def func(*_args, **_kwargs):
4747
class TestAzureMetricExporter(unittest.TestCase):
4848
@classmethod
4949
def setUpClass(cls):
50-
os.environ.clear()
50+
os.environ.pop("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL", None)
51+
os.environ.pop("APPINSIGHTS_INSTRUMENTATIONKEY", None)
5152
os.environ["APPINSIGHTS_INSTRUMENTATIONKEY"] = "1234abcd-5678-4efa-8abc-1234567890ab"
5253
os.environ["APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"] = "true"
5354
cls._exporter = AzureMonitorMetricExporter()

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/statsbeat/test_exporter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def clean_folder(folder):
4747
class TestStatsbeatExporter(unittest.TestCase):
4848
@classmethod
4949
def setUpClass(cls):
50-
os.environ.clear()
50+
os.environ.pop("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL", None)
51+
os.environ.pop("APPINSIGHTS_INSTRUMENTATIONKEY", None)
5152
os.environ["APPINSIGHTS_INSTRUMENTATIONKEY"] = "1234abcd-5678-4efa-8abc-1234567890ab"
5253
os.environ["APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"] = "false"
5354
cls._exporter = _StatsBeatExporter(

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/statsbeat/test_statsbeat.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,13 @@ def test_shutdown_statsbeat_metrics(self):
267267
class TestStatsbeatMetrics(unittest.TestCase):
268268
@classmethod
269269
def setUpClass(cls):
270-
os.environ.clear()
270+
os.environ.pop("WEBSITE_SITE_NAME", None)
271+
os.environ.pop("WEBSITE_HOME_STAMPNAME", None)
272+
os.environ.pop("FUNCTIONS_WORKER_RUNTIME", None)
273+
os.environ.pop("WEBSITE_HOSTNAME", None)
274+
os.environ.pop("AKS_ARM_NAMESPACE_ID", None)
275+
os.environ.pop("KUBERNETES_SERVICE_HOST", None)
276+
271277
mp = MeterProvider()
272278
ikey = "1aa11111-bbbb-1ccc-8ddd-eeeeffff3334"
273279
endpoint = "https://westus-1.in.applicationinsights.azure.com/"

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ class TestBaseExporter(unittest.TestCase):
7575
@classmethod
7676
def setUpClass(cls):
7777
# Clear environ so the mocks from past tests do not interfere.
78-
os.environ.clear()
78+
os.environ.pop("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL", None)
79+
os.environ.pop("APPINSIGHTS_INSTRUMENTATIONKEY", None)
7980
os.environ["APPINSIGHTS_INSTRUMENTATIONKEY"] = "1234abcd-5678-4efa-8abc-1234567890ab"
8081
os.environ["APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"] = "true"
8182
cls._base = BaseExporter()

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
# pylint: disable=too-many-public-methods
1111
class TestConnectionStringParser(unittest.TestCase):
1212
def setUp(self):
13-
os.environ.clear()
13+
os.environ.pop("APPLICATIONINSIGHTS_CONNECTION_STRING", None)
14+
os.environ.pop("APPINSIGHTS_INSTRUMENTATIONKEY", None)
1415
self._valid_connection_string = "InstrumentationKey=1234abcd-5678-4efa-8abc-1234567890ab"
1516
self._valid_instrumentation_key = "1234abcd-5678-4efa-8abc-1234567890ab"
1617

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,6 @@ def test_lease_error(self):
7979

8080
# pylint: disable=protected-access
8181
class TestLocalFileStorage(unittest.TestCase):
82-
@classmethod
83-
def setup_class(cls):
84-
os.makedirs(TEST_FOLDER, exist_ok=True)
8582

8683
@classmethod
8784
def tearDownClass(cls):

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

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

3434
class TestUtils(unittest.TestCase):
3535
def setUp(self):
36-
os.environ.clear()
3736
self._valid_instrumentation_key = "1234abcd-5678-4efa-8abc-1234567890ab"
3837

3938
def test_nanoseconds_to_duration(self):

0 commit comments

Comments
 (0)