Skip to content

Commit c9558a4

Browse files
Merge pull request #846 from microsoft/dev-v4
test: dev-v4 to main PR
2 parents 567f094 + d2e3902 commit c9558a4

38 files changed

+1261
-1325
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ omit =
1111
*/env/*
1212
*/.pytest_cache/*
1313
*/node_modules/*
14+
src/backend/v4/api/router.py
1415

1516
[paths]
1617
source =

.github/workflows/test.yml

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,29 @@ jobs:
6666
6767
- name: Run tests with coverage
6868
if: env.skip_tests == 'false'
69+
env:
70+
PYTHONPATH: src:src/backend
6971
run: |
70-
if python -m pytest src/tests/backend/test_app.py --cov=backend --cov-config=.coveragerc -q > /dev/null 2>&1 && \
71-
python -m pytest src/tests/backend --cov=backend --cov-append --cov-report=term --cov-report=xml --cov-config=.coveragerc --ignore=src/tests/backend/test_app.py; then
72-
echo "Tests completed, checking coverage."
73-
if [ -f coverage.xml ]; then
74-
COVERAGE=$(python -c "import xml.etree.ElementTree as ET; tree = ET.parse('coverage.xml'); root = tree.getroot(); print(float(root.attrib.get('line-rate', 0)) * 100)")
75-
echo "Overall coverage: $COVERAGE%"
76-
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
77-
echo "Coverage is below 80%, failing the job."
78-
exit 1
79-
fi
72+
# Run test_app.py first (isolation required)
73+
python -m pytest src/tests/backend/test_app.py --cov=src/backend --cov-config=.coveragerc -q
74+
75+
# Run remaining backend tests with coverage append
76+
python -m pytest src/tests/backend --cov=src/backend --cov-append --cov-report=term --cov-report=xml --cov-config=.coveragerc --ignore=src/tests/backend/test_app.py
77+
78+
- name: Check coverage threshold
79+
if: env.skip_tests == 'false'
80+
run: |
81+
if [ -f coverage.xml ]; then
82+
COVERAGE=$(python -c "import xml.etree.ElementTree as ET; tree = ET.parse('coverage.xml'); root = tree.getroot(); print(float(root.attrib.get('line-rate', 0)) * 100)")
83+
echo "Overall coverage: $COVERAGE%"
84+
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
85+
echo "::error::Coverage is below 80% threshold. Current: $COVERAGE%"
86+
exit 1
8087
fi
88+
echo "✅ Coverage threshold met: $COVERAGE% >= 80%"
8189
else
82-
echo "No tests found, skipping coverage check."
90+
echo "::error::coverage.xml not found"
91+
exit 1
8392
fi
8493
8594
- name: Skip coverage report if no tests

conftest.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,18 @@
77

88
import pytest
99

10-
# Add the agents path
11-
agents_path = Path(__file__).parent.parent.parent / "backend" / "v4" / "magentic_agents"
12-
sys.path.insert(0, str(agents_path))
10+
# Get the root directory of the project
11+
root_dir = Path(__file__).parent
12+
13+
# Add src directory to path for 'backend', 'common', 'v4' etc. imports
14+
src_path = root_dir / "src"
15+
if str(src_path) not in sys.path:
16+
sys.path.insert(0, str(src_path))
17+
18+
# Add src/backend to path for relative imports within backend
19+
backend_path = root_dir / "src" / "backend"
20+
if str(backend_path) not in sys.path:
21+
sys.path.insert(0, str(backend_path))
1322

1423
@pytest.fixture
1524
def agent_env_vars():

infra/main.bicep

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,6 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en
373373
flowType: 'Bluefield'
374374
// WAF aligned configuration for Monitoring
375375
workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : ''
376-
diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null
377376
}
378377
}
379378

infra/main.json

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
"metadata": {
66
"_generator": {
77
"name": "bicep",
8-
"version": "0.40.2.10011",
9-
"templateHash": "17476534152468179054"
8+
"version": "0.41.2.15936",
9+
"templateHash": "7834026170340721066"
1010
},
1111
"name": "Multi-Agent Custom Automation Engine",
12-
"description": "This module contains the resources required to deploy the [Multi-Agent Custom Automation Engine solution accelerator](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator) for both Sandbox environments and WAF aligned environments.\n\n> **Note:** This module is not intended for broad, generic use, as it was designed by the Commercial Solution Areas CTO team, as a Microsoft Solution Accelerator. Feature requests and bug fix requests are welcome if they support the needs of this organization but may not be incorporated if they aim to make this module more generic than what it needs to be for its primary use case. This module will likely be updated to leverage AVM resource modules in the future. This may result in breaking changes in upcoming versions when these features are implemented.\n"
12+
"description": "This module contains the resources required to deploy the [Multi-Agent Custom Automation Engine solution accelerator](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator) for both Sandbox environments and WAF aligned environments.\r\n\r\n> **Note:** This module is not intended for broad, generic use, as it was designed by the Commercial Solution Areas CTO team, as a Microsoft Solution Accelerator. Feature requests and bug fix requests are welcome if they support the needs of this organization but may not be incorporated if they aim to make this module more generic than what it needs to be for its primary use case. This module will likely be updated to leverage AVM resource modules in the future. This may result in breaking changes in upcoming versions when these features are implemented.\r\n"
1313
},
1414
"parameters": {
1515
"solutionName": {
@@ -3703,8 +3703,7 @@
37033703
"flowType": {
37043704
"value": "Bluefield"
37053705
},
3706-
"workspaceResourceId": "[if(parameters('enableMonitoring'), if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), createObject('value', reference('logAnalyticsWorkspace').outputs.resourceId.value)), createObject('value', ''))]",
3707-
"diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value)))), createObject('value', null()))]"
3706+
"workspaceResourceId": "[if(parameters('enableMonitoring'), if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), createObject('value', reference('logAnalyticsWorkspace').outputs.resourceId.value)), createObject('value', ''))]"
37083707
},
37093708
"template": {
37103709
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
@@ -4921,8 +4920,8 @@
49214920
"metadata": {
49224921
"_generator": {
49234922
"name": "bicep",
4924-
"version": "0.40.2.10011",
4925-
"templateHash": "16969845928384020185"
4923+
"version": "0.41.2.15936",
4924+
"templateHash": "8667922205584012198"
49264925
}
49274926
},
49284927
"definitions": {
@@ -22453,8 +22452,8 @@
2245322452
"metadata": {
2245422453
"_generator": {
2245522454
"name": "bicep",
22456-
"version": "0.40.2.10011",
22457-
"templateHash": "8742987061721021759"
22455+
"version": "0.41.2.15936",
22456+
"templateHash": "8365054813170845685"
2245822457
}
2245922458
},
2246022459
"definitions": {
@@ -25440,8 +25439,8 @@
2544025439
}
2544125440
},
2544225441
"dependsOn": [
25443-
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]",
2544425442
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]",
25443+
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]",
2544525444
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]",
2544625445
"logAnalyticsWorkspace",
2544725446
"userAssignedIdentity",
@@ -25481,8 +25480,8 @@
2548125480
"metadata": {
2548225481
"_generator": {
2548325482
"name": "bicep",
25484-
"version": "0.40.2.10011",
25485-
"templateHash": "7507285802464480889"
25483+
"version": "0.41.2.15936",
25484+
"templateHash": "5789718034225488560"
2548625485
}
2548725486
},
2548825487
"parameters": {
@@ -34461,8 +34460,8 @@
3446134460
"metadata": {
3446234461
"_generator": {
3446334462
"name": "bicep",
34464-
"version": "0.40.2.10011",
34465-
"templateHash": "8640881069237947782"
34463+
"version": "0.41.2.15936",
34464+
"templateHash": "14525082674956141939"
3446634465
}
3446734466
},
3446834467
"definitions": {
@@ -35474,8 +35473,8 @@
3547435473
"metadata": {
3547535474
"_generator": {
3547635475
"name": "bicep",
35477-
"version": "0.40.2.10011",
35478-
"templateHash": "10706743168754451638"
35476+
"version": "0.41.2.15936",
35477+
"templateHash": "1185169597469996118"
3547935478
},
3548035479
"name": "Site App Settings",
3548135480
"description": "This module deploys a Site App Setting."
@@ -44644,8 +44643,8 @@
4464444643
"metadata": {
4464544644
"_generator": {
4464644645
"name": "bicep",
44647-
"version": "0.40.2.10011",
44648-
"templateHash": "15348022841521786626"
44646+
"version": "0.41.2.15936",
44647+
"templateHash": "8488390916703184584"
4464944648
}
4465044649
},
4465144650
"parameters": {

infra/main_custom.bicep

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en
372372
flowType: 'Bluefield'
373373
// WAF aligned configuration for Monitoring
374374
workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : ''
375-
diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null
376375
}
377376
}
378377

src/backend/app.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from fastapi import FastAPI, Request
1818
from fastapi.middleware.cors import CORSMiddleware
1919

20+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
21+
2022
# Local imports
2123
from middleware.health_check import HealthCheckMiddleware
2224
from v4.api.router import app_v4
@@ -51,20 +53,6 @@ async def lifespan(app: FastAPI):
5153
logger.info("👋 MACAE application shutdown complete")
5254

5355

54-
# Check if the Application Insights Instrumentation Key is set in the environment variables
55-
connection_string = config.APPLICATIONINSIGHTS_CONNECTION_STRING
56-
if connection_string:
57-
# Configure Application Insights if the Instrumentation Key is found
58-
configure_azure_monitor(connection_string=connection_string)
59-
logging.info(
60-
"Application Insights configured with the provided Instrumentation Key"
61-
)
62-
else:
63-
# Log a warning if the Instrumentation Key is not found
64-
logging.warning(
65-
"No Application Insights Instrumentation Key found. Skipping configuration"
66-
)
67-
6856
# Configure logging levels from environment variables
6957
# logging.basicConfig(level=getattr(logging, config.AZURE_BASIC_LOGGING_LEVEL.upper(), logging.INFO))
7058

@@ -80,10 +68,32 @@ async def lifespan(app: FastAPI):
8068

8169
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.WARNING)
8270

71+
# Suppress noisy Azure Monitor exporter "Transmission succeeded" logs
72+
logging.getLogger("azure.monitor.opentelemetry.exporter.export._base").setLevel(logging.WARNING)
73+
8374
# Initialize the FastAPI app
8475
app = FastAPI(lifespan=lifespan)
8576

8677
frontend_url = config.FRONTEND_SITE_NAME
78+
# Configure Azure Monitor and instrument FastAPI for OpenTelemetry
79+
# This enables automatic request tracing, dependency tracking, and proper operation_id
80+
if config.APPLICATIONINSIGHTS_CONNECTION_STRING:
81+
# Configure Application Insights telemetry with live metrics
82+
configure_azure_monitor(
83+
connection_string=config.APPLICATIONINSIGHTS_CONNECTION_STRING,
84+
enable_live_metrics=True
85+
)
86+
87+
# Instrument FastAPI app — exclude WebSocket URLs to reduce telemetry noise
88+
FastAPIInstrumentor.instrument_app(
89+
app,
90+
excluded_urls="socket,ws"
91+
)
92+
logging.info("Application Insights configured with live metrics and WebSocket filtering")
93+
else:
94+
logging.warning(
95+
"No Application Insights connection string found. Telemetry disabled."
96+
)
8797

8898
# Add this near the top of your app.py, after initializing the app
8999
app.add_middleware(

src/backend/common/config/app_config.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from azure.ai.projects.aio import AIProjectClient
77
from azure.cosmos import CosmosClient
88
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
9+
from azure.identity.aio import (
10+
DefaultAzureCredential as DefaultAzureCredentialAsync,
11+
ManagedIdentityCredential as ManagedIdentityCredentialAsync,
12+
)
913
from dotenv import load_dotenv
1014

1115

@@ -113,7 +117,8 @@ def get_azure_credential(self, client_id=None):
113117
"""
114118
Returns an Azure credential based on the application environment.
115119
116-
If the environment is 'dev', it uses DefaultAzureCredential.
120+
If the environment is 'dev', it uses DefaultAzureCredential with exclude_environment_credential=True
121+
to avoid EnvironmentCredential exceptions in Application Insights traces.
117122
Otherwise, it uses ManagedIdentityCredential.
118123
119124
Args:
@@ -123,10 +128,29 @@ def get_azure_credential(self, client_id=None):
123128
Credential object: Either DefaultAzureCredential or ManagedIdentityCredential.
124129
"""
125130
if self.APP_ENV == "dev":
126-
return DefaultAzureCredential() # CodeQL [SM05139]: DefaultAzureCredential is safe here
131+
return DefaultAzureCredential(exclude_environment_credential=True) # CodeQL [SM05139]: DefaultAzureCredential is safe here
127132
else:
128133
return ManagedIdentityCredential(client_id=client_id)
129134

135+
def get_azure_credential_async(self, client_id=None):
136+
"""
137+
Returns an async Azure credential based on the application environment.
138+
139+
If the environment is 'dev', it uses DefaultAzureCredential (async) with exclude_environment_credential=True
140+
to avoid EnvironmentCredential exceptions in Application Insights traces.
141+
Otherwise, it uses ManagedIdentityCredential (async).
142+
143+
Args:
144+
client_id (str, optional): The client ID for the Managed Identity Credential.
145+
146+
Returns:
147+
Async Credential object: Either DefaultAzureCredentialAsync or ManagedIdentityCredentialAsync.
148+
"""
149+
if self.APP_ENV == "dev":
150+
return DefaultAzureCredentialAsync(exclude_environment_credential=True)
151+
else:
152+
return ManagedIdentityCredentialAsync(client_id=client_id)
153+
130154
def get_azure_credentials(self):
131155
"""Retrieve Azure credentials, either from environment variables or managed identity."""
132156
if self._azure_credentials is None:

0 commit comments

Comments
 (0)