diff --git a/.gitignore b/.gitignore index 8be5a27..c7d539a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ bld/ [Oo]bj/ [Ll]og/ [Ll]ogs/ +publish/ # .NET Core project.lock.json diff --git a/samples/function-app-service-bus/dotnet/README.md b/samples/function-app-service-bus/dotnet/README.md index 31b19d5..c61f53b 100644 --- a/samples/function-app-service-bus/dotnet/README.md +++ b/samples/function-app-service-bus/dotnet/README.md @@ -81,7 +81,99 @@ All deployment methods have been fully tested against Azure and the LocalStack f ## Test -Once the resources and function app have been deployed, you can use the [call-http-trigger.sh](./scripts/call-http-trigger.sh) Bash script to invoke the **GetGreetings** HTTP-triggered function. This function returns the most recent greetings stored in the in-memory circular buffer, allowing you to verify that the entire message pipeline is working end to end. +Once the resources and function app have been deployed, you can use the [call-http-trigger.sh](./scripts/call-http-trigger.sh) Bash script to invoke the **GetGreetings** HTTP-triggered function. This function returns the most recent greetings stored in the in-memory circular buffer, allowing you to verify that the entire message pipeline is working end to end. The output should look like this: + +```bash +Getting function app name... +Function app [local-func-test] successfully retrieved. +Getting resource group name for function app [local-func-test]... +Resource group [local-rg] successfully retrieved. +Getting the default host name of the function app [local-func-test]... +Function app default host name [local-func-test.azurewebsites.azure.localhost.localstack.cloud:4566] successfully retrieved. +Finding container name with prefix [ls-local-func-test]... +Looking for containers with names starting with [ls-local-func-test]... +Found matching container [ls-local-func-test-tdkqjh] +Container [ls-local-func-test-tdkqjh] found successfully +Getting IP address for container [ls-local-func-test-tdkqjh]... +IP address [172.17.0.7] retrieved successfully for container [ls-local-func-test-tdkqjh] +Getting the host port mapped to internal port 80 in container [ls-local-func-test-tdkqjh]... +Mapped host port [42330] retrieved successfully for container [ls-local-func-test-tdkqjh] +Calling HTTP trigger function to retrieve the last [10] greetings via emulator... +{ + "requester": { + "sent": [ + "Paolo", + "Max" + ] + }, + "handler": { + "received": [ + "Paolo", + "Max" + ], + "sent": [ + "Welcome Paolo, glad you're here!", + "Salutations Max, how's everything going?" + ] + }, + "consumer": { + "received": [ + "Welcome Paolo, glad you're here!", + "Salutations Max, how's everything going?" + ] + } +} +Calling HTTP trigger function to retrieve the last [10] greetings via container IP address [172.17.0.7]... +{ + "requester": { + "sent": [ + "Paolo", + "Max" + ] + }, + "handler": { + "received": [ + "Paolo", + "Max" + ], + "sent": [ + "Welcome Paolo, glad you're here!", + "Salutations Max, how's everything going?" + ] + }, + "consumer": { + "received": [ + "Welcome Paolo, glad you're here!", + "Salutations Max, how's everything going?" + ] + } +} +Calling HTTP trigger function to retrieve the last [10] greetings via host port [42330]... +{ + "requester": { + "sent": [ + "Paolo", + "Max" + ] + }, + "handler": { + "received": [ + "Paolo", + "Max" + ], + "sent": [ + "Welcome Paolo, glad you're here!", + "Salutations Max, how's everything going?" + ] + }, + "consumer": { + "received": [ + "Welcome Paolo, glad you're here!", + "Salutations Max, how's everything going?" + ] + } +} +``` You can also inspect the function app's runtime behavior by viewing the logs of its Docker container. Run `docker logs ls-local-func-test-xxxxxx` (replacing `xxxxxx` with the actual container suffix) to see output similar to the following: diff --git a/samples/function-app-service-bus/dotnet/scripts/deploy.sh b/samples/function-app-service-bus/dotnet/scripts/deploy.sh index ea7b736..d68fa06 100755 --- a/samples/function-app-service-bus/dotnet/scripts/deploy.sh +++ b/samples/function-app-service-bus/dotnet/scripts/deploy.sh @@ -716,6 +716,26 @@ if [ $DEPLOY -eq 0 ]; then exit 0 fi +# Check if the application insights az extension is already installed +echo "Checking if [application-insights] az extension is already installed..." +az extension show --name application-insights &>/dev/null + +if [[ $? == 0 ]]; then + echo "[application-insights] az extension is already installed" +else + echo "[application-insights] az extension is not installed. Installing..." + + # Install application-insights az extension + az extension add --name application-insights 1>/dev/null + + if [[ $? == 0 ]]; then + echo "[application-insights] az extension successfully installed" + else + echo "Failed to install [application-insights] az extension" + exit + fi +fi + # Check if the application insights component already exists echo "Checking if [$APPLICATION_INSIGHTS_NAME] Application Insights component exists in the [$RESOURCE_GROUP_NAME] resource group..." az monitor app-insights component show \ diff --git a/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs b/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs index 8e0a41b..bbe47f8 100644 --- a/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs +++ b/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs @@ -46,12 +46,13 @@ public class HelloWorld private static readonly Random _random = new(); - // Circular buffer storing the last 100 greetings - private const int MaxGreetingHistory = 100; - private static readonly string[] _greetingHistory = new string[MaxGreetingHistory]; - private static int _greetingIndex = 0; - private static int _greetingCount = 0; - private static readonly object _greetingLock = new(); + // Circular buffers for message history across all functions + private const int MaxHistory = 100; + private static readonly object _historyLock = new(); + private static readonly CircularBuffer _requesterSent = new(MaxHistory); + private static readonly CircularBuffer _handlerReceived = new(MaxHistory); + private static readonly CircularBuffer _handlerSent = new(MaxHistory); + private static readonly CircularBuffer _consumerReceived = new(MaxHistory); // Static initialization - runs once per application lifetime private static readonly Lazy _initialized = new Lazy(() => { Initialize(); return true; }); @@ -116,7 +117,7 @@ private static void Initialize() } } - /// + /// /// Validates that all required configuration values are present and not empty. /// With default values in place, only the connection string is mandatory. /// @@ -214,6 +215,12 @@ private static bool IsConfigurationValid() _logger.LogInformation("[GreetingHandler] Processing request for name: {name}", requestMessage.Name); + // Record received name in history + lock (_historyLock) + { + _handlerReceived.Add(requestMessage.Name); + } + // Create the response message var greetingText = GetGreeting(requestMessage.Name); var outputObj = new ResponseMessage @@ -287,6 +294,12 @@ public async Task GreetingRequesterAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOn _logger.LogInformation("[GreetingRequester] Sending message to input queue '{inputQueue}'...", _inputQueueName); await sender.SendMessageAsync(serviceBusMessage); _logger.LogInformation("[GreetingRequester] Successfully sent message to input queue '{inputQueue}' with name: {Name}", _inputQueueName, selectedName); + + // Record sent name in history + lock (_historyLock) + { + _requesterSent.Add(selectedName); + } } catch (Exception ex) { @@ -320,7 +333,7 @@ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnS } try - { + { // Create Service Bus client for receiving messages from the output queue _logger.LogInformation("[GreetingConsumer] Creating Service Bus client for receiving messages..."); await using var client = _hasClientId && _hasFullyQualifiedNamespace @@ -364,6 +377,12 @@ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnS // Complete the message after successful processing await receiver.CompleteMessageAsync(receivedMessage); + + // Record received greeting in history + lock (_historyLock) + { + _consumerReceived.Add(responseMessage.Text); + } } else { @@ -387,17 +406,18 @@ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnS finally { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - try - { - await receiver.CloseAsync(cts.Token); + try + { + await receiver.CloseAsync(cts.Token); } catch { /* timeout or error on close */ } - try - { - await client.DisposeAsync(); - } - catch { /* benign */ + try + { + await client.DisposeAsync(); + } + catch + { /* benign */ } } } @@ -421,12 +441,9 @@ private static string GetGreeting(string name) var template = _greetingTemplates[_random.Next(_greetingTemplates.Length)]; var greeting = string.Format(template, name); - lock (_greetingLock) + lock (_historyLock) { - _greetingHistory[_greetingIndex] = greeting; - _greetingIndex = (_greetingIndex + 1) % MaxGreetingHistory; - if (_greetingCount < MaxGreetingHistory) - _greetingCount++; + _handlerSent.Add(greeting); } return greeting; @@ -444,31 +461,66 @@ public async Task GetGreetingsAsync( [HttpTrigger(AuthorizationLevel.Function, "get", Route = "greetings")] HttpRequestData request, int count = 20) { - _logger.LogInformation("[GetGreetings] Retrieving last {count} greetings.", count); + _logger.LogInformation("[GetGreetings] Retrieving last {count} entries.", count); // Clamp count to valid range if (count < 1) count = 1; - if (count > MaxGreetingHistory) count = MaxGreetingHistory; + if (count > MaxHistory) count = MaxHistory; - string[] result; - lock (_greetingLock) + object history; + lock (_historyLock) { - var available = Math.Min(count, _greetingCount); - result = new string[available]; - - // Read backwards from the most recent entry - for (int i = 0; i < available; i++) + history = new { - var idx = (_greetingIndex - 1 - i + MaxGreetingHistory) % MaxGreetingHistory; - result[i] = _greetingHistory[idx]; - } + requester = new + { + sent = _requesterSent.ToArray(count) + }, + handler = new + { + received = _handlerReceived.ToArray(count), + sent = _handlerSent.ToArray(count) + }, + consumer = new + { + received = _consumerReceived.ToArray(count) + } + }; } var response = request.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "application/json"); - await response.WriteStringAsync(JsonSerializer.Serialize(result)); + await response.WriteStringAsync(JsonSerializer.Serialize(history)); return response; } + + private sealed class CircularBuffer + { + private readonly string[] _items; + private int _index; + private int _count; + + public CircularBuffer(int capacity) => _items = new string[capacity]; + + public void Add(string item) + { + _items[_index] = item; + _index = (_index + 1) % _items.Length; + if (_count < _items.Length) _count++; + } + + public string[] ToArray(int count) + { + var available = Math.Min(count, _count); + var result = new string[available]; + for (int i = 0; i < available; i++) + { + var idx = (_index - available + i + _items.Length) % _items.Length; + result[i] = _items[idx]; + } + return result; + } + } } ///