Skip to content

Commit 38c2a60

Browse files
authored
Added ENV support for middleware.ini properties (#43)
1 parent ad54324 commit 38c2a60

File tree

6 files changed

+247
-32
lines changed

6 files changed

+247
-32
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
instrumentation_env
55
\source\middleware-apm
66
\source\__pycache__
7+
**/__pycache__
78
\source\apmpython.pyc
89
test_env
910
.idea
1011
\apmpythonpackage
11-
venv
12+
venv
13+
.vscode

middleware/config.py

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,69 @@
44

55

66
class Config:
7-
def __init__(self):
7+
def get_config(self, section, key, default):
8+
# Allowing OTEL level overrides at top priority
9+
if key in self.otel_config_binding:
10+
otel_env_value = os.environ.get(self.otel_config_binding[key], None)
11+
if otel_env_value is not None and otel_env_value != "":
12+
return otel_env_value
13+
14+
# Allowing MW level ENV overrides
15+
if key in self.config_binding:
16+
env_value = os.environ.get(self.config_binding[key], None)
17+
if env_value is not None and env_value != "":
18+
return env_value
19+
20+
return self.config.get(section, key, fallback=default)
21+
22+
def str_to_bool(self, value, default):
23+
if isinstance(value, str):
24+
value = value.strip().lower()
25+
if value in {"true", "1", "yes", "y"}:
26+
return True
27+
elif value in {"false", "0", "no", "n"}:
28+
return False
29+
return default # Default value if the string cannot be converted
30+
31+
def get_config_boolean(self, section, key, default):
32+
# Allowing OTEL level overrides at top priority
33+
if key in self.otel_config_binding:
34+
otel_env_value = os.environ.get(self.otel_config_binding[key], None)
35+
if otel_env_value is not None and otel_env_value != "":
36+
return self.str_to_bool(otel_env_value, default)
37+
38+
# Allowing MW level ENV overrides
39+
if key in self.config_binding:
40+
env_value = os.environ.get(self.config_binding[key], None)
41+
if env_value is not None and env_value != "":
42+
return self.str_to_bool(env_value, default)
43+
44+
return self.config.getboolean(section, key, fallback=default)
45+
46+
def __init__(self):
847
if len(sys.argv) > 1 and sys.argv[1] == "help":
948
return
49+
50+
self.config_binding = {
51+
"project_name": "MW_PROJECT_NAME",
52+
"service_name": "MW_SERVICE_NAME",
53+
"access_token": "MW_API_KEY",
54+
"collect_traces": "MW_APM_COLLECT_TRACES",
55+
"collect_metrics": "MW_APM_COLLECT_METRICS",
56+
"collect_logs": "MW_APM_COLLECT_LOGS",
57+
"collect_profiling": "MW_APM_COLLECT_PROFILING",
58+
"otel_propagators": "MW_PROPAGATORS",
59+
"mw_agent_service": "MW_AGENT_SERVICE",
60+
"target": "MW_TARGET",
61+
"custom_resource_attributes": "MW_CUSTOM_RESOURCE_ATTRIBUTES",
62+
"log_level": "MW_LOG_LEVEL",
63+
}
64+
65+
self.otel_config_binding = {
66+
"service_name": "OTEL_SERVICE_NAME",
67+
"otel_propagators": "OTEL_PROPAGATORS",
68+
"target": "OTEL_EXPORTER_OTLP_ENDPOINT"
69+
}
1070

1171
config_file = os.environ.get("MIDDLEWARE_CONFIG_FILE", os.path.join(os.getcwd(), 'middleware.ini'))
1272
if not os.path.exists(config_file):
@@ -21,48 +81,43 @@ def __init__(self):
2181
pid = os.getpid()
2282
self.project_name = self.get_config("middleware.common", "project_name", None)
2383
self.service_name = self.get_config("middleware.common", "service_name", f"Service-{pid}")
84+
self.mw_agent_service = self.get_config("middleware.common", "mw_agent_service", "localhost")
85+
self.target = self.get_config("middleware.common", "target", "")
2486
self.access_token = self.get_config("middleware.common", "access_token", "")
2587
self.collect_traces = self.get_config_boolean("middleware.common", "collect_traces", True)
2688
self.collect_metrics = self.get_config_boolean("middleware.common", "collect_metrics", False)
2789
self.collect_logs = self.get_config_boolean("middleware.common", "collect_logs", False)
2890
self.collect_profiling = self.get_config_boolean("middleware.common", "collect_profiling", False)
2991
self.otel_propagators = self.get_config("middleware.common", "otel_propagators", "b3")
92+
self.custom_resource_attributes = self.get_config("middleware.common", "custom_resource_attributes", "")
93+
self.log_level = self.get_config("middleware.common", "log_level", "FATAL")
3094

31-
project_name_attr = f"project.name={self.project_name}," if self.project_name else ""
32-
source_service_url = self.get_config("middleware.common", "mw_agent_service", "localhost")
33-
34-
mw_agent_service = os.environ.get("MW_AGENT_SERVICE", None)
35-
if mw_agent_service is not None and mw_agent_service != "":
36-
source_service_url = mw_agent_service
37-
38-
self.exporter_otlp_endpoint = f"http://{source_service_url}:9319"
39-
self.resource_attributes = f"{project_name_attr}mw.app.lang=python,runtime.metrics.python=true"
95+
# target will have more priority over mw_agent_service
96+
self.exporter_otlp_endpoint = f"http://{self.mw_agent_service}:9319"
97+
if self.target is not None and self.target != "":
98+
self.exporter_otlp_endpoint = self.target
4099

41-
# Allowing users to override full OTLP endpoint
42-
# Priority OTEL_EXPORTER_OTLP_ENDPOINT > MW_TARGET
43-
mw_target = os.environ.get("MW_TARGET", None)
44-
if mw_target is not None and mw_target != "":
45-
self.exporter_otlp_endpoint = mw_target
46-
47-
exporter_otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", None)
48-
if exporter_otlp_endpoint is not None and exporter_otlp_endpoint != "":
49-
self.exporter_otlp_endpoint = exporter_otlp_endpoint
50100

51-
# Allowing users to pass Middleware API Key via ENV variable
52-
mw_api_key = os.environ.get("MW_API_KEY", None)
53-
if mw_api_key is not None and mw_api_key != "":
54-
self.access_token = mw_api_key
101+
self.resource_attributes = "mw.app.lang=python,runtime.metrics.python=true"
102+
103+
# Add `mw_serverless` resource attribute if target is not "localhost"
104+
if "localhost" not in self.exporter_otlp_endpoint and "127.0.0.1" not in self.exporter_otlp_endpoint:
105+
self.resource_attributes = f"{self.resource_attributes},mw_serverless=true"
106+
107+
# Passing Project name as a resource attribute
108+
if self.project_name is not None and self.project_name != "":
109+
self.resource_attributes = f"{self.resource_attributes},project.name={self.project_name}"
55110

56111
# Passing Middleware API Key as a resource attribute, to validate ingestion requests in serverless setup
57112
if self.access_token is not None and self.access_token != "":
58113
self.resource_attributes = f"{self.resource_attributes},mw.account_key={self.access_token}"
59-
60-
def get_config(self, section, key, default):
61-
return self.config.get(section, key, fallback=default)
62-
63-
def get_config_boolean(self, section, key, default):
64-
return self.config.getboolean(section, key, fallback=default)
65-
114+
115+
# Appending Custom Resource Attributes, if any
116+
if self.custom_resource_attributes is not None and self.custom_resource_attributes != "":
117+
self.resource_attributes = f"{self.resource_attributes},{self.custom_resource_attributes}"
118+
119+
120+
66121

67122
def exit_with_error(message):
68123
print(message)

middleware/config_test.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import unittest
2+
from unittest.mock import patch, mock_open
3+
import os
4+
import sys
5+
from configparser import ConfigParser
6+
from config import Config # Replace 'your_module' with the name of your module
7+
import os
8+
class TestConfig(unittest.TestCase):
9+
10+
def setUp(self):
11+
# Save the original environment variables
12+
self._original_env = dict(os.environ)
13+
14+
def tearDown(self):
15+
# Restore the original environment variables
16+
os.environ.clear()
17+
os.environ.update(self._original_env)
18+
19+
# If config file is empty && envs are not set, properties should be set to defaults
20+
def test_empty_config(self):
21+
# Passing test config values
22+
import os
23+
os.environ["MIDDLEWARE_CONFIG_FILE"] = os.path.join(os.getcwd(), 'testfiles/middleware_empty.ini')
24+
obj = Config()
25+
26+
self.assertEqual(obj.project_name, None)
27+
# self.asse(obj.service_name, None)
28+
self.assertEqual(obj.access_token, "")
29+
self.assertEqual(obj.collect_traces, True)
30+
self.assertEqual(obj.collect_metrics, False)
31+
self.assertEqual(obj.collect_logs, False)
32+
self.assertEqual(obj.collect_profiling, False)
33+
self.assertEqual(obj.otel_propagators, "b3")
34+
self.assertEqual(obj.mw_agent_service, "localhost")
35+
self.assertEqual(obj.target, "")
36+
self.assertEqual(obj.custom_resource_attributes, "")
37+
self.assertEqual(obj.log_level, "FATAL")
38+
39+
def test_env_overrides(self):
40+
# Setting configs with Middleware specific
41+
os.environ["MIDDLEWARE_CONFIG_FILE"] = os.path.join(os.getcwd(), 'testfiles/middleware_default.ini')
42+
os.environ["MW_PROJECT_NAME"] = "project123"
43+
os.environ["MW_SERVICE_NAME"] = "service123"
44+
os.environ["MW_API_KEY"] = "xxxyyyzzz"
45+
os.environ["MW_APM_COLLECT_TRACES"] = "false"
46+
os.environ["MW_APM_COLLECT_METRICS"] = "true"
47+
os.environ["MW_APM_COLLECT_LOGS"] = "true"
48+
os.environ["MW_APM_COLLECT_PROFILING"] = "true"
49+
os.environ["MW_PROPAGATORS"] = "w3c"
50+
os.environ["MW_TARGET"] = "http://test.middleware.io"
51+
os.environ["MW_CUSTOM_RESOURCE_ATTRIBUTES"] = "test=123,test2=1234"
52+
os.environ["MW_LOG_LEVEL"] = "DEBUG"
53+
54+
obj = Config()
55+
self.assertEqual(obj.project_name, "project123")
56+
self.assertEqual(obj.service_name, "service123")
57+
self.assertEqual(obj.access_token, "xxxyyyzzz")
58+
self.assertEqual(obj.collect_traces, False)
59+
self.assertEqual(obj.collect_metrics, True)
60+
self.assertEqual(obj.collect_logs, True)
61+
self.assertEqual(obj.collect_profiling, True)
62+
self.assertEqual(obj.otel_propagators, "w3c")
63+
self.assertEqual(obj.mw_agent_service, "localhost")
64+
self.assertEqual(obj.target, "http://test.middleware.io")
65+
self.assertEqual(obj.custom_resource_attributes, "test=123,test2=1234")
66+
self.assertEqual(obj.log_level, "DEBUG")
67+
68+
def test_otel_env_overrides(self):
69+
# Middleware Specific ENVs
70+
os.environ["MIDDLEWARE_CONFIG_FILE"] = os.path.join(os.getcwd(), 'testfiles/middleware_default.ini')
71+
os.environ["MW_PROJECT_NAME"] = "project123"
72+
os.environ["MW_SERVICE_NAME"] = "service123"
73+
os.environ["MW_API_KEY"] = "xxxyyyzzz"
74+
os.environ["MW_APM_COLLECT_TRACES"] = "false"
75+
os.environ["MW_APM_COLLECT_METRICS"] = "true"
76+
os.environ["MW_APM_COLLECT_LOGS"] = "true"
77+
os.environ["MW_APM_COLLECT_PROFILING"] = "true"
78+
os.environ["MW_PROPAGATORS"] = "w3c"
79+
os.environ["MW_TARGET"] = "http://test.middleware.io"
80+
os.environ["MW_CUSTOM_RESOURCE_ATTRIBUTES"] = "test=123,test2=1234"
81+
os.environ["MW_LOG_LEVEL"] = "DEBUG"
82+
83+
# OTEL ENVs
84+
os.environ["OTEL_SERVICE_NAME"] = "otel-service123"
85+
os.environ["OTEL_PROPAGATORS"] = "otel-b3"
86+
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://otel-test.middleware.io"
87+
88+
obj = Config()
89+
self.assertEqual(obj.project_name, "project123")
90+
self.assertEqual(obj.service_name, "otel-service123")
91+
self.assertEqual(obj.access_token, "xxxyyyzzz")
92+
self.assertEqual(obj.collect_traces, False)
93+
self.assertEqual(obj.collect_metrics, True)
94+
self.assertEqual(obj.collect_logs, True)
95+
self.assertEqual(obj.collect_profiling, True)
96+
self.assertEqual(obj.otel_propagators, "otel-b3")
97+
self.assertEqual(obj.mw_agent_service, "localhost")
98+
self.assertEqual(obj.target, "http://otel-test.middleware.io")
99+
self.assertEqual(obj.custom_resource_attributes, "test=123,test2=1234")
100+
self.assertEqual(obj.log_level, "DEBUG")
101+
102+
def test_str_to_bool(self):
103+
config = Config()
104+
self.assertTrue(config.str_to_bool('true', False))
105+
self.assertFalse(config.str_to_bool('false', True))
106+
self.assertTrue(config.str_to_bool('yes', False))
107+
self.assertFalse(config.str_to_bool('no', True))
108+
self.assertTrue(config.str_to_bool('1', False))
109+
self.assertFalse(config.str_to_bool('0', True))
110+
self.assertEqual(config.str_to_bool('invalid', True), True)
111+
self.assertEqual(config.str_to_bool('', True), True)
112+
113+
def test_mw_serverless(self):
114+
115+
os.environ["MW_SERVICE_NAME"] = "service123"
116+
os.environ["MW_TARGET"] = "http://test.middleware.io"
117+
obj = Config()
118+
self.assertIn("mw_serverless",obj.resource_attributes)
119+
120+
def test_not_mw_serverless(self):
121+
os.environ["MW_SERVICE_NAME"] = "service1234"
122+
os.environ["MIDDLEWARE_CONFIG_FILE"] = os.path.join(os.getcwd(), 'testfiles/middleware_default.ini')
123+
obj = Config()
124+
self.assertNotIn("mw_serverless",obj.resource_attributes)
125+
126+
if __name__ == '__main__':
127+
unittest.main()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# ---------------------------------------------------------------------------
2+
# This file contains settings for the Middleware Python-APM Agent.
3+
# Here are the settings that are common to all environments.
4+
# ---------------------------------------------------------------------------
5+
6+
[middleware.common]
7+
8+
# The name of your application as service-name, as it will appear in the UI to filter out your data.
9+
service_name = Python-APM-Service
10+
11+
project_name = check
12+
13+
# This Token binds the Python Agent's data and profiling data to your account.
14+
access_token = *** REPLACE ME ***
15+
16+
# The service name, where Middleware Agent is running, in case of K8s.
17+
;mw_agent_service = mw-service.mw-agent-ns.svc.cluster.local
18+
19+
# Toggle to enable/disable distributed traces for your application.
20+
collect_traces = true
21+
22+
# Toggle to enable/disable the collection of metrics for your application.
23+
collect_metrics = false
24+
25+
# Toggle to enable/disable the collection of logs for your application.
26+
collect_logs = false
27+
28+
# Toggle to enable/disable the collection of profiling data for your application.
29+
collect_profiling = false
30+
31+
# ---------------------------------------------------------------------------

middleware/testfiles/middleware_empty.ini

Whitespace-only changes.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
setuptools.setup(
2121
name="middleware-apm",
22-
version="1.0.0",
22+
version="1.1.0",
2323
install_requires=requirements,
2424
author="middleware-dev",
2525
maintainer="middleware-dev",

0 commit comments

Comments
 (0)