Azure Functions – Health Endpoint Implementation
Summary
This post outlines the design and implementation of an HTTP-based API for managing Customer Purchases using Azure Functions. The system also includes a Health Check endpoint to monitor the availability of the API and its dependencies.
Azure Functions Health Endpoint
2. Introduction
In addition, the system integrates with Azure Application Insights to provide logging and monitoring for tracking API performance and usage.
This document serves as a working implementation of: EIP – Health Endpoint.
2. Definitions
- Customer Purchase: A record representing a customer transaction, including payment details such as card number, sort code, and amount.
- CRUD: Refers to the basic operations (Create, Read, Update, Delete) performed on resources.
- Health Check: An endpoint that provides the operational status of the system.
- Application Insights: A feature of Azure Monitor that provides monitoring, logging, and analytics for Azure applications.
3. Overview of the API
The Customer Purchase API is a RESTful service that supports operations for managing customer purchases. The API consists of the following:
- Create Purchase: Adds a new customer purchase.
- Read Purchase: Retrieves details of a specific purchase.
- Update Purchase: Modifies an existing purchase.
- Delete Purchase: Removes a purchase from the system.
- Health Check: Monitors the health of the API.
API Versioning
The API follows versioning, with the current version set to v1
. All endpoints are prefixed with /api/v1/
.
4. System Design
4.1. API Routes
The following table outlines the API endpoints:
HTTP Method | Endpoint | Description |
---|---|---|
POST | /api/v1/purchases | Creates a new customer purchase |
GET | /api/v1/purchases/{id} | Retrieves a purchase by ID |
PUT | /api/v1/purchases/{id} | Updates an existing purchase |
DELETE | /api/v1/purchases/{id} | Deletes a purchase |
GET | /api/v1/health | Returns the health status of the API |
4.2. Data Models
The API handles the CustomerPurchase model, which contains the following fields:
{
"PurchaseId": "string",
"CustomerId": "string",
"ProductId": "string",
"CardNumber": "string",
"SortCode": "string",
"Amount": "decimal",
"PurchaseDate": "DateTime"
}
PurchaseId
: A unique identifier for the purchase (auto-generated).CustomerId
: The ID of the customer making the purchase.ProductId
: The ID of the product being purchased.CardNumber
: The customer’s card number (obfuscated in production).SortCode
: The bank sort code.Amount
: The amount of the purchase.PurchaseDate
: The date and time of the purchase.
4.3. Error Handling
Error responses will follow the HTTP status codes as outlined below:
200 OK
: Operation was successful.201 Created
: A new resource has been successfully created.400 Bad Request
: The request was malformed or missing required data.404 Not Found
: The requested resource does not exist.500 Internal Server Error
: A server error occurred.
Example of an error response for a 404 Not Found
:
{
"error": "Purchase not found"
}
4.4. Health Check
The Health Check endpoint provides a basic status of the API. It returns a structured response:
GET /api/v1/health
{
"status": "healthy",
"timestamp": "2024-10-08T10:45:00Z"
}
A 200 OK
response indicates the service is operational, while other statuses or errors indicate potential issues.
5. Azure Application Insights Integration
The API uses Azure Application Insights for logging and monitoring:
- Logging: The system captures key events such as purchases being created, updated, or deleted. These logs are sent to Application Insights for real-time monitoring.
- Custom Telemetry: Each CRUD operation logs custom events, such as:
Purchase Created: PurchaseId by CustomerId
Purchase Updated: PurchaseId
Purchase Deleted: PurchaseId
- Request Tracing: All HTTP requests, including success and failure, are tracked.
To enable Application Insights in the Azure Functions project:
- Enable Application Insights in the Azure portal for the Function App.
- The logs will automatically appear in the Application Insights section, providing insights into performance, failures, and dependencies.
6. Security Considerations
- Sensitive Data: Card numbers and sort codes must be encrypted or obfuscated before storage. In this example, the API simply simulates storing the data without encryption, but production implementations should integrate a secure payment processing service.
- API Authentication: The API should use API keys or OAuth 2.0 for securing sensitive endpoints. In this case, the CRUD operations are protected by a function key (
AuthorizationLevel.Function
), but in production, you should consider stricter security measures. - Data Validation: Inputs must be validated to prevent SQL injection, XSS, or other attacks.
7. Future Extensions
The API can be extended in several ways:
- Database Integration: Replace the in-memory store with a persistent data store such as Azure SQL or Cosmos DB.
- Pagination and Filtering: Add support for paginated queries and filtering on the
GET /purchases
endpoint. - Webhook Support: Provide webhook callbacks for purchase-related events (e.g., notifying external systems after a purchase is created).
- Authentication: Integrate more robust security, such as OAuth or JWT-based authentication.
- Rate Limiting: Implement rate limiting to prevent abuse of the API.
8. Appendix
8.1. Example Request: Creating a Customer Purchase
- Request:
POST /api/v1/purchases
Content-Type: application/json
{
"CustomerId": "12345",
"ProductId": "product-abc-123",
"CardNumber": "4111111111111111",
"SortCode": "123456",
"Amount": 100.50
}
- Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"PurchaseId": "8baff234-456e-431f-a813-00013afed67d",
"CustomerId": "12345",
"ProductId": "product-abc-123",
"CardNumber": "4111111111111111",
"SortCode": "123456",
"Amount": 100.50,
"PurchaseDate": "2024-10-08T11:00:00Z"
}
8.2. Example Request: Health Check
- Request:
GET /api/v1/health
- Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "healthy",
"timestamp": "2024-10-08T11:00:00Z"
}
Full working Code
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
namespace HealthEndpoint
{
public class Function1
{
private readonly ILogger<Function1> _logger;
// In-memory storage for purchases
private static List<CustomerPurchase> _purchases = new List<CustomerPurchase>();
public Function1(ILogger<Function1> log)
{
_logger = log;
}
// Simulating CRUD operations for CustomerPurchase
[FunctionName("CreatePurchase")]
[OpenApiOperation(operationId: "CreatePurchase", tags: new[] { "purchases" })]
[OpenApiRequestBody("application/json", typeof(CustomerPurchase))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(CustomerPurchase), Description = "Purchase created successfully")]
public async Task<IActionResult> CreatePurchase(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/v1/purchases")] HttpRequest req)
{
_logger.LogInformation("Received request to create a new purchase.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var purchase = JsonConvert.DeserializeObject<CustomerPurchase>(requestBody);
purchase.PurchaseId = Guid.NewGuid().ToString();
purchase.PurchaseDate = DateTime.UtcNow;
_purchases.Add(purchase);
_logger.LogInformation($"Purchase created: {purchase.PurchaseId} by Customer: {purchase.CustomerId}");
return new OkObjectResult(purchase);
}
[FunctionName("GetPurchaseById")]
[OpenApiOperation(operationId: "GetPurchaseById", tags: new[] { "purchases" })]
[OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Description = "The ID of the purchase to fetch")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(CustomerPurchase), Description = "Purchase details")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Description = "Purchase not found")]
public IActionResult GetPurchaseById(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/v1/purchases/{id}")] HttpRequest req,
string id)
{
_logger.LogInformation($"Fetching purchase details for ID: {id}");
var purchase = _purchases.FirstOrDefault(p => p.PurchaseId == id);
if (purchase == null)
{
return new NotFoundObjectResult("Purchase not found");
}
return new OkObjectResult(purchase);
}
[FunctionName("UpdatePurchase")]
[OpenApiOperation(operationId: "UpdatePurchase", tags: new[] { "purchases" })]
[OpenApiRequestBody("application/json", typeof(CustomerPurchase))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(CustomerPurchase), Description = "Purchase updated successfully")]
public async Task<IActionResult> UpdatePurchase(
[HttpTrigger(AuthorizationLevel.Function, "put", Route = "api/v1/purchases/{id}")] HttpRequest req,
string id)
{
_logger.LogInformation($"Received request to update purchase with ID: {id}");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var updatedPurchase = JsonConvert.DeserializeObject<CustomerPurchase>(requestBody);
var purchase = _purchases.FirstOrDefault(p => p.PurchaseId == id);
if (purchase == null)
{
return new NotFoundObjectResult("Purchase not found");
}
purchase.CardNumber = updatedPurchase.CardNumber;
purchase.SortCode = updatedPurchase.SortCode;
purchase.Amount = updatedPurchase.Amount;
_logger.LogInformation($"Purchase updated: {purchase.PurchaseId}");
return new OkObjectResult(purchase);
}
[FunctionName("DeletePurchase")]
[OpenApiOperation(operationId: "DeletePurchase", tags: new[] { "purchases" })]
[OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Description = "The ID of the purchase to delete")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.OK, Description = "Purchase deleted successfully")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Description = "Purchase not found")]
public IActionResult DeletePurchase(
[HttpTrigger(AuthorizationLevel.Function, "delete", Route = "api/v1/purchases/{id}")] HttpRequest req,
string id)
{
_logger.LogInformation($"Received request to delete purchase with ID: {id}");
var purchase = _purchases.FirstOrDefault(p => p.PurchaseId == id);
if (purchase == null)
{
return new NotFoundObjectResult("Purchase not found");
}
_purchases.Remove(purchase);
_logger.LogInformation($"Purchase deleted: {purchase.PurchaseId}");
return new OkResult();
}
// Health Check Endpoint
[FunctionName("HealthCheck")]
[OpenApiOperation(operationId: "HealthCheck", tags: new[] { "health" })]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(HealthCheckResponse), Description = "Health check response")]
public IActionResult HealthCheck(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "api/v1/health")] HttpRequest req)
{
_logger.LogInformation("Executing health check.");
var response = new HealthCheckResponse
{
Status = "healthy",
Timestamp = DateTime.UtcNow
};
return new OkObjectResult(response);
}
}
// HealthCheck Response Model
public class HealthCheckResponse
{
public string Status { get; set; }
public DateTime Timestamp { get; set; }
}
// CustomerPurchase Model
public class CustomerPurchase
{
public string PurchaseId { get; set; }
public string CustomerId { get; set; }
public string ProductId { get; set; }
public string CardNumber { get; set; }
public string SortCode { get; set; }
public decimal Amount { get; set; }
public DateTime PurchaseDate { get; set; }
}
}