Skip to content

Commit fee5ff9

Browse files
authored
Merge pull request #47 from microsoft/shshr/ObservabilityUpdate
Update Agent Observability + Security Update for Container Apps Env
2 parents a6e1066 + 275e810 commit fee5ff9

15 files changed

Lines changed: 379 additions & 85 deletions

infra/core/host/container-apps-environment.bicep

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ param logAnalyticsWorkspaceId string
1313
@description('Whether to enable zone redundancy')
1414
param zoneRedundant bool = false
1515

16-
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
16+
@description('The resource ID of the subnet for Container Apps infrastructure')
17+
param infrastructureSubnetId string
18+
19+
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = {
1720
name: name
1821
location: location
1922
tags: tags
@@ -25,6 +28,16 @@ resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01'
2528
sharedKey: listKeys(logAnalyticsWorkspaceId, '2021-12-01-preview').primarySharedKey
2629
}
2730
}
31+
vnetConfiguration: {
32+
internal: false // Must be false to have public IP address as per security requirements
33+
infrastructureSubnetId: infrastructureSubnetId
34+
}
35+
workloadProfiles: [
36+
{
37+
name: 'Consumption'
38+
workloadProfileType: 'Consumption'
39+
}
40+
]
2841
zoneRedundant: zoneRedundant
2942
}
3043
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@description('The name of the NAT gateway')
2+
param name string
3+
4+
@description('The location for the NAT gateway')
5+
param location string = resourceGroup().location
6+
7+
@description('The tags to apply to the NAT gateway')
8+
param tags object = {}
9+
10+
@description('The name of the public IP address')
11+
param publicIpName string
12+
13+
@description('Idle timeout in minutes for the NAT gateway')
14+
param idleTimeoutInMinutes int = 4
15+
16+
resource publicIp 'Microsoft.Network/publicIPAddresses@2023-09-01' = {
17+
name: publicIpName
18+
location: location
19+
tags: tags
20+
sku: {
21+
name: 'Standard'
22+
tier: 'Regional'
23+
}
24+
properties: {
25+
publicIPAllocationMethod: 'Static'
26+
publicIPAddressVersion: 'IPv4'
27+
}
28+
}
29+
30+
resource natGateway 'Microsoft.Network/natGateways@2023-09-01' = {
31+
name: name
32+
location: location
33+
tags: tags
34+
sku: {
35+
name: 'Standard'
36+
}
37+
properties: {
38+
publicIpAddresses: [
39+
{
40+
id: publicIp.id
41+
}
42+
]
43+
idleTimeoutInMinutes: idleTimeoutInMinutes
44+
}
45+
}
46+
47+
output natGatewayId string = natGateway.id
48+
output natGatewayName string = natGateway.name
49+
output publicIpId string = publicIp.id
50+
output publicIpAddress string = publicIp.properties.ipAddress
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
@description('The name of the virtual network')
2+
param name string
3+
4+
@description('The location for the virtual network')
5+
param location string = resourceGroup().location
6+
7+
@description('The tags to apply to the virtual network')
8+
param tags object = {}
9+
10+
@description('The address space for the virtual network')
11+
param addressSpace string = '10.0.0.0/16'
12+
13+
@description('The name of the subnet for Container Apps')
14+
param subnetName string = 'container-apps-subnet'
15+
16+
@description('The address prefix for the Container Apps subnet')
17+
param subnetAddressPrefix string = '10.0.0.0/23'
18+
19+
@description('The resource ID of the NAT gateway to attach to the subnet')
20+
param natGatewayId string
21+
22+
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
23+
name: name
24+
location: location
25+
tags: tags
26+
properties: {
27+
addressSpace: {
28+
addressPrefixes: [
29+
addressSpace
30+
]
31+
}
32+
subnets: [
33+
{
34+
name: subnetName
35+
properties: {
36+
addressPrefix: subnetAddressPrefix
37+
natGateway: {
38+
id: natGatewayId
39+
}
40+
serviceEndpoints: []
41+
delegations: [
42+
{
43+
name: 'Microsoft.App.environments'
44+
properties: {
45+
serviceName: 'Microsoft.App/environments'
46+
}
47+
type: 'Microsoft.Network/virtualNetworks/subnets/delegations'
48+
}
49+
]
50+
}
51+
type: 'Microsoft.Network/virtualNetworks/subnets'
52+
}
53+
]
54+
virtualNetworkPeerings: []
55+
enableDdosProtection: false
56+
}
57+
}
58+
59+
output vnetId string = virtualNetwork.id
60+
output vnetName string = virtualNetwork.name
61+
output subnetId string = virtualNetwork.properties.subnets[0].id
62+
output subnetName string = subnetName

infra/main.bicep

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,31 @@ module containerRegistry './core/host/container-registry.bicep' = {
195195
}
196196
}
197197

198-
// Create Container Apps Environment
198+
// Create NAT Gateway with public IP for Container Apps
199+
module natGateway './core/networking/nat-gateway.bicep' = {
200+
name: 'nat-gateway'
201+
scope: rg
202+
params: {
203+
name: '${abbrs.networkNatGateways}${resourceToken}'
204+
location: location
205+
tags: tags
206+
publicIpName: '${abbrs.networkPublicIPAddresses}${resourceToken}'
207+
}
208+
}
209+
210+
// Create Virtual Network with subnet for Container Apps
211+
module virtualNetwork './core/networking/virtual-network.bicep' = {
212+
name: 'virtual-network'
213+
scope: rg
214+
params: {
215+
name: '${abbrs.networkVirtualNetworks}${resourceToken}'
216+
location: location
217+
tags: tags
218+
natGatewayId: natGateway.outputs.natGatewayId
219+
}
220+
}
221+
222+
// Create Container Apps Environment with subnet and NAT gateway
199223
module containerAppsEnvironment './core/host/container-apps-environment.bicep' = {
200224
name: 'container-apps-environment'
201225
scope: rg
@@ -204,6 +228,7 @@ module containerAppsEnvironment './core/host/container-apps-environment.bicep' =
204228
location: location
205229
tags: tags
206230
logAnalyticsWorkspaceId: monitoring.outputs.logAnalyticsWorkspaceId
231+
infrastructureSubnetId: virtualNetwork.outputs.subnetId
207232
}
208233
}
209234

@@ -352,6 +377,13 @@ output FRONTEND_URL string = staticWebApp.outputs.uri
352377
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
353378
output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name
354379

380+
// Network infrastructure outputs
381+
output VIRTUAL_NETWORK_ID string = virtualNetwork.outputs.vnetId
382+
output VIRTUAL_NETWORK_NAME string = virtualNetwork.outputs.vnetName
383+
output SUBNET_ID string = virtualNetwork.outputs.subnetId
384+
output NAT_GATEWAY_ID string = natGateway.outputs.natGatewayId
385+
output PUBLIC_IP_ADDRESS string = natGateway.outputs.publicIpAddress
386+
355387
@secure()
356388
output AZURE_AI_FOUNDRY_RESOURCE_GROUP string = azureAiFoundryResourceGroup
357389
@secure()

src/backend/common/agent_factory/agent_base.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def get_instance(cls: Type[T], logger: AppLogger, tracer_provider: AppTrac
4848

4949
async with cls._locks[cls]:
5050
if cls not in cls._instances:
51-
cls._instances[cls] = cls(logger)
51+
cls._instances[cls] = cls(logger, tracer_provider)
5252

5353
return cls._instances[cls]
5454

@@ -156,10 +156,7 @@ async def run(
156156
if tools and isinstance(tools, MCPStreamableHTTPTool):
157157
use_async_context = True
158158

159-
with self._tracer_provider.trace_agent_run(
160-
session_id=session_id,
161-
agent_name=self._agent.name
162-
):
159+
with self._tracer_provider.trace_agent_run(session_id=session_id, agent_name=self._agent.name):
163160
if use_async_context:
164161
async with tools:
165162
agent_response = await self._agent.run(
@@ -168,7 +165,7 @@ async def run(
168165
tools=tools,
169166
response_format=response_format,
170167
max_tokens=runtime_configuration.max_completion_tokens,
171-
model=runtime_configuration.model,
168+
model_id=runtime_configuration.model,
172169
temperature=runtime_configuration.temperature,
173170
top_p=runtime_configuration.top_p,
174171
**kwargs
@@ -181,7 +178,7 @@ async def run(
181178
tools=tools,
182179
response_format=response_format,
183180
max_tokens=runtime_configuration.max_completion_tokens,
184-
model=runtime_configuration.model,
181+
model_id=runtime_configuration.model,
185182
temperature=runtime_configuration.temperature,
186183
top_p=runtime_configuration.top_p,
187184
**kwargs
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
from .app_logger import AppLogger
5+
from .app_tracer_provider import AppTracerProvider
6+
from .log_classes import LogProperties
7+
8+
__all__ = [
9+
"AppLogger",
10+
"AppTracerProvider",
11+
"LogProperties"
12+
]

src/backend/common/telemetry/app_logger.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
88
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
99
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
10-
from opentelemetry.sdk.resources import Resource, ResourceAttributes
10+
from opentelemetry.sdk.resources import Resource
1111
from opentelemetry._logs import set_logger_provider
1212

1313

1414
from common.telemetry.log_classes import LogProperties
1515

16-
resource = Resource.create({ResourceAttributes.SERVICE_NAME: "telemetry"})
16+
DEFAULT_RESOURCE = Resource.create({"resource.name": "telemetry"})
1717

1818
class LogEvent(Enum):
1919
REQUEST_RECEIVED = "Request.Received"
@@ -28,40 +28,86 @@ class ConsoleLogFilter(logging.Filter):
2828
def __init__(self):
2929
super().__init__()
3030
self.base_dir = os.path.abspath(os.path.join(__file__, "..", ".."))
31+
3132
# Define allowed third-party loggers (only show WARNING and above)
3233
self.allowed_third_party = [
3334
"semantic_kernel",
34-
"agent_framework",
35+
"agent_framework",
3536
"azure_mcp"
3637
]
37-
38+
3839
def filter(self, record):
3940
# Always allow logs from our application
4041
if os.path.abspath(record.pathname).startswith(self.base_dir):
4142
return True
42-
43+
4344
# For third-party libraries, only show WARNING and above to reduce noise
4445
for allowed in self.allowed_third_party:
4546
if record.name.startswith(allowed):
4647
return record.levelno >= logging.WARNING
47-
48+
4849
# Filter out everything else
4950
return False
5051

5152
class AppLogger:
52-
def __init__(self, connection_string: str):
53-
self.connection_string = connection_string
53+
def __init__(self, connection_string: str = None, logger: logging.Logger = None, resource: Resource = None):
54+
"""
55+
Initialize AppLogger with either a connection string or an existing logger.
5456
55-
logging.getLogger("azure.identity").setLevel(logging.WARNING)
56-
logging.getLogger("azure.core.pipeline.policies").setLevel(logging.WARNING)
57-
logging.getLogger("azure.monitor.opentelemetry.exporter.export").setLevel(logging.WARNING)
57+
Args:
58+
connection_string: Azure Monitor connection string for telemetry (optional if logger is provided)
59+
logger: Existing logger instance to wrap (optional if connection_string is provided)
60+
resource: Resource describing the service (optional)
61+
"""
62+
if logger is not None:
63+
# Initialize from existing logger
64+
self.connection_string = connection_string
65+
self.logger = logger
66+
self.logger_provider = LoggerProvider(resource or DEFAULT_RESOURCE)
67+
self._from_existing_logger = True
68+
69+
self.logger.setLevel(logging.INFO)
70+
71+
# Set up third-party logger levels
72+
logging.getLogger("azure.identity").setLevel(logging.WARNING)
73+
logging.getLogger("azure.core.pipeline.policies").setLevel(logging.WARNING)
74+
logging.getLogger("azure.monitor.opentelemetry.exporter.export").setLevel(logging.WARNING)
75+
76+
# Only initialize Azure Monitor if connection string is provided
77+
if connection_string:
78+
self.initialize_loggers()
79+
else:
80+
# Original initialization path
81+
if connection_string is None:
82+
raise ValueError("Either connection_string or logger must be provided")
83+
84+
self.connection_string = connection_string
85+
self._from_existing_logger = False
86+
87+
logging.getLogger("azure.identity").setLevel(logging.WARNING)
88+
logging.getLogger("azure.core.pipeline.policies").setLevel(logging.WARNING)
89+
logging.getLogger("azure.monitor.opentelemetry.exporter.export").setLevel(logging.WARNING)
90+
91+
self.logger = logging.getLogger()
92+
self.logger.setLevel(logging.INFO)
93+
self.logger_provider = LoggerProvider(resource)
94+
95+
self.initialize_loggers()
96+
97+
@classmethod
98+
def from_logger(cls, logger: logging.Logger, connection_string: str = None, resource: Resource = None) -> "AppLogger":
99+
"""
100+
Create an AppLogger instance from an existing logger.
58101
59-
self.logger = logging.getLogger()
60-
self.logger.setLevel(logging.INFO)
61-
self.logger_provider = LoggerProvider(resource)
102+
Args:
103+
logger: Existing logger instance to wrap
104+
connection_string: Optional Azure Monitor connection string for telemetry
105+
106+
Returns:
107+
AppLogger instance that wraps the provided logger
108+
"""
109+
return cls(connection_string=connection_string, logger=logger, resource=resource)
62110

63-
self.initialize_loggers()
64-
65111
def initialize_loggers(self):
66112
if self.connection_string:
67113
if not any(
@@ -73,7 +119,7 @@ def initialize_loggers(self):
73119
self.handler = LoggingHandler()
74120
self.logger.addHandler(self.handler)
75121

76-
# add console logger if it is not already added by another instance of CustomLogger
122+
# Only add console handler if we're not using an existing logger or if no StreamHandler exists
77123
if not any(
78124
isinstance(handler, logging.StreamHandler)
79125
for handler in self.logger.handlers
@@ -85,7 +131,9 @@ def initialize_loggers(self):
85131
console_handler.setLevel(log_level)
86132
console_handler.addFilter(ConsoleLogFilter())
87133
self.logger.addHandler(console_handler)
88-
set_logger_provider(self.logger_provider)
134+
135+
if not self._from_existing_logger:
136+
set_logger_provider(self.logger_provider)
89137

90138
def info(self, message:str, properties: dict = None):
91139
self.logger.info(message)

0 commit comments

Comments
 (0)