Integrar Azure API Management con Log Analytics

Uno de los escenarios en el que he estado trabajando estos días es en integrar API Management con Log Analytics. Si bien es cierto que no existe ninguna solución en la galería de OMS o integración directa entre los dos servicios he creado una solución que me permite registrar toda información que me interesa de las llamadas realizadas a mi API Management y mandarlas a OMS de una forma bastante elegante.

Crear Azure Event Hubs

Lo primero que necesitas es crear un servicio del tipo Event Hub con el fin de poder enviar a algún sitio toda la información que se va a ir registrando según las peticiones recibidas. Para ello basta con acceder al portal de Azure y a través del botón Create a resource  crear un nuevo servicio del tipo Event Hub. Esta acción sólo te creará el namespace por lo que necesitas crear además un event hub dentro de este. Haz clic en el botón + Event Hub en el apartado Overview. Para este ejemplo he creado uno llamado oms.

Create new Event Hub

Por último necesitas crear un par de políticas de acceso. Selecciona tu nuevo event hub, en mi caso el llamado oms, y en el apartado Shared access policies crea dos: una con el permiso Send (yo la he llamado Sending) y otra con el permiso Listen (llamada en mi caso Receiving).

Event Hub oms – Shared access policies

Crear Logger para API Management

Una vez que tienes el servicio de Event Hub activado el siguiente paso es dar de alta un nuevo logger para tu API Management. La forma de hacerlo a día de hoy es llamando directamente a la API REST, para ello puedes utilizar Fiddler, Postman o cualquier otro cliente que te permita hacer peticiones HTTP/S.

Para construir esta llamada necesitas acceso a la API de administración de API Management. Desde el portal de Azure puedes habilitar el acceso a ella en la sección Management API de tu servicio.

De aquí debes recuperar la Management API URL y generar un Access Token en la parte inferior de la pantalla.

APIM – Management API

En mi caso he utilizado Postman para hacer la llamada y esta es la configuración que debes utilizar:

La llamada es un PUT a la URL https://TU_API_MANAGEMENT.management.azure-api.net/loggers/NOMBRE_LOGGER/?api-version=2016-10-10. El cuerpo del mensaje debe seguir este formato:

{
  "type" : "azureEventHub",
  "description" : "OMS logger",
  "credentials" : {
    "name" : "YOUR_EVENT_HUB_NAME",
    "connectionString" : "YOU_EVENT_HUB_CONNECTION_STRING_WITH_SEND_CLAIM"
    }
}

Postman PUT APIM Logger – Body

En cuanto a la cabecera de la llamada debe contener el Content-Type: application/jsonAuthorization: con la clave que generaste en el apartado Management API. Si la llamada es correcta devolverá un 201, confirmando que el recurso ha sido creado con éxito:

Postman PUT APIM Logger – Headers

Crear política que registre las llamadas a las APIs

Ya tenemos las herramientas necesarias para almacenar y registrar eventos desde API Management. El siguiente paso es insertar la política que nos permita hacer el registro de los valores que consideremos y enviarlos a Event Hub a través de tu nuevo logger. Las políticas de API Management pueden asignarse tanto a un Producto, como a una API o incluso a una acción en particular. En este ejemplo vamos a hacerlo para todas las APIs de un producto, en este caso el llamado Free. Accede al apartado Products de API Management, selecciona el producto del que desear registrar eventos y haz clic en la sección Policies:

APIM – Products – Policies

Si nunca has estado en este apartado, se trata de un archivo XML donde defines las politicas de entrada y de salida para un Producto, una API o una acción. Para más información sobre politicas puedes acceder a este enlace.

En este caso la politica que nos interesa es la llamada log-to-eventhub al cual debemos especificarle el nombre del logger y como contenido el texto, información, etcétera que queremos enviar al event hub que tenemos asociado con el logger. Para esta prueba he creado la politica de esta manera:

<!--
    IMPORTANT:
    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.
    - Only the <forward-request> policy element can appear within the <backend> section element.
    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.
    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.
    - To add a policy position the cursor at the desired insertion point and click on the round button associated with the policy.
    - To remove a policy, delete the corresponding policy statement from the policy document.
    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.
    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.
    - Policies are applied in the order of their appearance, from the top down.
-->
<policies>
  <inbound>
    <set-variable name="message-id" value="@(Guid.NewGuid())" />
    <log-to-eventhub logger-id="oms">
      @{
          var headers = context.Request.Headers
          .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value)))
          .ToArray<string>();
      
          var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty;
                      
          var json = string.Format("{{'Date' : '{0}','Type': 'Request', 'Product' : '{1}', 'API' : '{2}', 'User' : '{3} {4}', 'Subscription' : '{5}', 'IP' : '{6}', 'URL' : '{7}', 'Method' : '{8}', 'Headers' : '{9}', 'Id' : '{10}'}}", DateTime.UtcNow.ToString("o"),context.Product.Name,context.Api.Name, context.User.FirstName, context.User.LastName,context.Subscription.Name, context.Request.IpAddress,context.Request.Url, context.Request.Method, headerString, context.Variables["message-id"]);
          return json;
      }
    </log-to-eventhub>
    <base />
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <log-to-eventhub logger-id="oms">
      @{
            var headers = context.Response.Headers
                          .Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value)))
                          .ToArray<string>();
            var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty;
            var json = string.Format("{{'Date' : '{0}','Type': 'Response', 'StatusCode' : '{1}', 'StatusReason' : '{2}', 'Headers' : '{3}', 'Id' : '{4}', 'Product' : '{5}', 'API' : '{6}', 'User' : '{7} {8}', 'Subscription' : '{9}' }}", DateTime.UtcNow.ToString("o"), context.Response.StatusCode, context.Response.StatusReason, headerString, context.Variables["message-id"],context.Product.Name,context.Api.Name, context.User.FirstName,context.User.LastName,context.Subscription.Name); 
      
            return json;
      }
    </log-to-eventhub>
    <base />
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

Como puedes ver, estoy recuperando ciertos valores del contexto de la llamada, tanto de request como de response con el objetivo de tener una foto completa de cada petición. De hecho, enlazo los dos eventos que se envían a Event Hub a través de un id que genero a través de la política set-variable. En este enlace puedes ver todos los valores que puedes recuperar del contexto, en el apartado Context Variable.

Capturar eventos con Azure Functions

Ya tenemos a API Management registrando en Event Hubs todas las llamadas que está recibiendo. El último paso es recuperar dichos eventos y enviarlos a OMS, que era el objetivo de este post. Para ello he utilizado el servicio Azure Functions con el trigger de Event Hub que me facilita mucho la tarea. El código para procesar la información es este:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;

// Update customerId to your Operations Management Suite workspace ID
static string customerId = "YOUR_CUSTOMER_ID";

// For sharedKey, use either the primary or the secondary Connected Sources client authentication key   
static string sharedKey = "YOUR_PRIMARY_KEY";

// LogName is name of the event type that is being submitted to Log Analytics
static string LogName = "APIManagement";

// You can use an optional field to specify the timestamp from the data. If the time field is not specified, Log Analytics assumes the time is the message ingestion time
static string TimeStampField = "";

public static void Run(string myEventHubMessage, TraceWriter log)
{
    log.Info($"C# Event Hub trigger function processed a message: {myEventHubMessage}");

    var datestring = DateTime.UtcNow.ToString("r");
    var jsonBytes = Encoding.UTF8.GetBytes(myEventHubMessage);
    string stringToHash = "POST\n" + jsonBytes.Length + "\napplication/json\n" + "x-ms-date:" + datestring + "\n/api/logs";
    string hashedString = BuildSignature(stringToHash, sharedKey);
    string signature = "SharedKey " + customerId + ":" + hashedString;

     PostDataToOMS(signature, datestring, myEventHubMessage ,log);
}

 // Build the API signature
public static string BuildSignature(string message, string secret)
{
    var encoding = new System.Text.ASCIIEncoding();
    byte[] keyByte = Convert.FromBase64String(secret);
    byte[] messageBytes = encoding.GetBytes(message);
    using (var hmacsha256 = new HMACSHA256(keyByte))
    {
        byte[] hash = hmacsha256.ComputeHash(messageBytes);
        return Convert.ToBase64String(hash);
    }
}

public static void PostDataToOMS(string signature, string date, string json, TraceWriter log)
{
    log.Info($"Signature: {signature}");
    log.Info($"Date: {date}");
    log.Info($"Json: {json}");

    try
    {
        string url = "https://" + customerId + ".ods.opinsights.azure.com/api/logs?api-version=2016-04-01";

        System.Net.Http.HttpClient client = new System.Net.Http.HttpClient();
        client.DefaultRequestHeaders.Add("Accept", "application/json");
        client.DefaultRequestHeaders.Add("Log-Type", LogName);
        client.DefaultRequestHeaders.Add("Authorization", signature);
        client.DefaultRequestHeaders.Add("x-ms-date", date);
        client.DefaultRequestHeaders.Add("time-generated-field", TimeStampField);

        HttpContent httpContent = new StringContent(json, Encoding.UTF8);
        httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        Task<HttpResponseMessage> response = client.PostAsync(new Uri(url), httpContent);

        HttpContent responseContent = response.Result.Content;
        string result = responseContent.ReadAsStringAsync().Result;
        log.Info($"Status: {response.Status}");
        log.Info($"Return Result: {result}");
    }
    catch (Exception excep)
    {
        log.Info("API Post Exception: " + excep.Message);
    }
}

El customer ID y la shared Key puedes recuperarlas en el servicio de Log Analytics > Advanced Settings > Connected Sources > Windows Servers.

OMS – Workspace ID & Primary Key

Para poder procesar la información del event hub que está asociado al logger de API Management debes asegurarte que en el apartado Integrate de tu Azure Function esté conectado con tu event hub y que la cardinalidad sea de tipo One.

Azure Function – ApimEventsToOMSFunction

Log Search

Una vez que API Management empiece a recibir llamadas al producto que tiene asociada la politica y tu Azure Function comience a procesar los eventos que va recibiendo en Log Analytics > Log Search podrás comprobar que los registros van llegando a través de la búsqueda search * | where Type == “APIManagement_CL” 

Log Analytics – Log Search – APIManagement_CL

¡Saludos!