Windows Azure Storage Analytics

El 3 de Agosto se anunció una nueva funcionalidad llamada Windows Azure Storage Analytics, la cual nos permitirá obtener trazas y métricas de una cuenta de Windows Azure Storage. Con esta nueva característica podremos obtener trazas de las peticiones, analizar el uso de nuestro almacenamiento así como diagnosticar posibles issues. Este servicio tiene un límite de 20TB, independientemente de los 100TB de nuestra cuenta, por lo que requerirá de algún tipo de mantenimiento que veremos después 😀

Debido a que a día de hoy la librería Microsoft.WindowsAzure.StorageClient.dll no contiene aún esta mejora, voy a dedicar este post a mostraros cuál sería la forma de configurar tanto las métricas como las trazas en una cuenta de Windows Azure Storage. Para ello, he creado la siguiente aplicación, la cual iremos viendo paso a paso 🙂

Tanto el sistema de métricas como de logging debe ser configurado a nivel de servicio, es decir a nivel de blobs, tablas y queues. Los logs serán almacenados en blobs dentro de una nueva carpeta llamada $logs y las métricas hará uso del servicio tables, utilizando dos tablas por servicio, por ejemplo $MetricsTransactionsBlob y $MetricsCapacityBlob. La primera de las tablas guardará información sobre las transacciones relacionadas contra los blobs de la cuenta, la segunda nos informará del uso de los mismos.

Recuperación de la configuración de un servicio

La configuración de cada uno de los servicios del storage está representada a través de un XML.

<?xml version="1.0" encoding="utf-8"?>
<StorageServiceProperties>
  <Logging>
    <Version>1.0</Version>
    <Read>false</Read>
    <Write>false</Write>
    <Delete>false</Delete>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Logging>
  <Metrics>
    <Enabled>true</Enabled>
    <Version>1.0</Version>
    <IncludeAPIs>true</IncludeAPIs>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Metrics>
</StorageServiceProperties>

Este XML es indivisible, ya que no podemos enviar la configuración de logging sin especificar el apartado de las métricas y viceversa. Para recuperar este formato, he creado una serie de objectos para poder mapear cada uno de los elementos a su correspondiente propiedad. Si desglosamos dicho XML, obtendríamos 4 clases:

StorageServiceProperties

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "StorageServiceProperties")]
    public class StorageServiceProperties
    {
        public StorageServiceProperties()
        {
            Logging = new Logging();
            Metrics = new Metrics();
        }

        [XmlIgnore]
        public string ServiceName { get; set; }

        [XmlElement("Logging")]
        public Logging Logging { get; set; }

        [XmlElement("Metrics")]
        public Metrics Metrics { get; set; }
    }
}

Logging

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "Logging")]
    public class Logging
    {
        public Logging()
        {
            RetentionPolicy = new RetentionPolicy();
            Version = "1.0";
        }

        [XmlElement("Version")]
        public string Version { get; set; }

        [XmlElement("Read")]
        public bool IsReadEnabled { get; set; }

        [XmlElement("Write")]
        public bool IsWriteEnabled { get; set; }

        [XmlElement("Delete")]
        public bool IsDeleteEnabled { get; set; }

        [XmlElement("RetentionPolicy")]
        public RetentionPolicy RetentionPolicy { get; set; }
    }
}

Metrics

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "Metrics")]
    public class Metrics
    {
        public Metrics()
        {
            RetentionPolicy = new RetentionPolicy();
            Version = "1.0";
        }

        [XmlElement("Enabled")]
        public bool Enabled { get; set; }

        [XmlElement("Version")]
        public string Version { get; set; }

        [XmlIgnore]
        [XmlElement("IncludeAPIs")]
        public bool IncludeAPIs { get; set; }

        [XmlElement("RetentionPolicy")]
        public RetentionPolicy RetentionPolicy { get; set; }
    }
}

RetentionPolicy

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "RetentionPolicy")]
    public class RetentionPolicy
    {
        [XmlElement("Enabled")]
        public bool IsEnabled { get; set; }

        [XmlElement("Days")]
        public long Days { get; set; }
    }
}

Entraremos más en detalle en cada una de ellas cuando llegue el momento de elegir la configuración. Por el momento, lo más importante es ser capaces de recuperar lo que tenemos establecido antes de comenzar a utilizar el servicio (por defecto todo estará deshabilitado).

Gracias a estas clases seremos capaces de trabajar de una forma orientada a objetos, facilitando la compresión del código y minimizando las iteraciones a través del XML. ¿Cómo conseguimos recuperar la infomación dentro de nuestras clases?  Para este escenario he creado una aplicación ASP.NET MVC 3, la cual tendrá un controlador llamado SettingsController. En él vamos a tener las siguientes acciones para interactuar con el servicio:

using System.Web.Mvc;
using MvcWebRole.Models;
using MvcWebRole.Models.Implementations;
using MvcWebRole.Models.Interfaces;
using MvcWebRole.Models.Objects;

namespace MvcWebRole.Controllers
{
    public class SettingsController : Controller
    {
        private readonly IWindowsAzureStorageService _windowsAzureStorageService;
        public SettingsController()
        {
            _windowsAzureStorageService = new WindowsAzureStorageService("YourAccountName", "YourPrimaryKeyAccessKey");
        }

        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Settings(string service)
        {
            return PartialView("_Settings", _windowsAzureStorageService.GetSettings(service));
        }

        [HttpPost]
        public ActionResult SetSettings(StorageServiceProperties storageServiceProperties)
        {
            _windowsAzureStorageService.SetSettings(storageServiceProperties);

            return RedirectToAction("Settings", new { service = storageServiceProperties.ServiceName });
        }
    }
}

Como cualquier buena aplicación con ASP.NET MVC que se precie, el controlador solamente se va a encargar de realizar las peticiones oportunas a una nueva clase llamada WindowsAzureStorageService la cual va a ser la encargada de realizar las peticiones a la plataforma. En este punto del post vamos a centrarnos en la acción Settings la cual recuperará los valores para el servicio que pasemos como parámetro al método GetSettings.

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using Microsoft.WindowsAzure;
using MvcWebRole.Models.Interfaces;
using MvcWebRole.Models.Objects;

namespace MvcWebRole.Models.Implementations
{
    public class WindowsAzureStorageService : IWindowsAzureStorageService
    {
        private readonly StorageCredentialsAccountAndKey _credentials;
        public WindowsAzureStorageService(string accountName, string accountKey)
        {
            _credentials = new StorageCredentialsAccountAndKey(accountName, accountKey);
        }

        public StorageServiceProperties GetSettings(string serviceName)
        {
            var request = MakeRequest("http://{0}.{1}.core.windows.net/?restype=service&comp=properties", serviceName, "GET");

            var xmlResponse = XDocument.Parse(new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd());

            var xmlSerializer = new XmlSerializer(typeof(StorageServiceProperties));

            var reader = new StringReader(xmlResponse.ToString());

            var storageServiceProperties = (StorageServiceProperties)xmlSerializer.Deserialize(reader);

            storageServiceProperties.ServiceName = serviceName;

            return storageServiceProperties;
        }
[...]

Utilizando las credenciales pasadas a través del constructor de WindowsAzureStorageService, procedemos a realizar una petición a la siguiente URL: http://accountName.ServiceName.core.windows.net/?restype=service&comp=properties. En esta URL especificamos para qué servicio estamos solicitando la configuración. Para facilitar la compresión del código, se ha refactorizado el proceso en un método llamado MakeRequest que será útil tanto para recibir como enviar la configuración de nuestra cuenta.

private HttpWebRequest MakeRequest(string uri, string serviceName, string action = "PUT")
{
    var request = (HttpWebRequest)HttpWebRequest.Create(string.Format(uri, _credentials.AccountName, serviceName));
    request.Method = action;
    request.Headers.Add("x-ms-version", "2009-09-19");

    if (action == "GET")
    {
        if (serviceName == "table") _credentials.SignRequestLite(request);
        else _credentials.SignRequest(request);
    }

    return request;
}

En este método crearemos un objeto del tipo HttpWebRequest donde además añadiremos la cabecera x-ms-version con valor 2009-09-19. Cuando solicitamos una configuración, el Http Method a utilizar será GET, ya que queremos obtener un recurso alojando en algún sitio. Para que la petición sea aceptada de manera satisfactoria, firmaremos la misma utilizando el objeto StorageCredentialsAccountAndKey. El siguiente paso será realizar la petición. Como somos conscientes del resultado de la misma, se ha utilizado XDocumnet para parsear la respuesta como xml. Para finalizar, debemos deserializar el resultado haciendo uso de las clases creadas anteriormnete a través de un objeto XmlSerializer como se muestra en el código anterior.

El resultado será parecido a lo siguiente:

¡Perfecto! Ya tenemos la configuración actual lista para mostrarla por pantalla.

Asignar la configuración a un servicio

La recuperación de los datos no es nada preocupante, ya que sólamente debemos realizar una petición GET y esperar a que llegue el resultado 😉 Cuando queremos especificar ciertos valores a nuestro storage la cosa se complica, aunque al final sólo bastará con tener en cuenta una serie de factores para crear una buena petición 😀

Loggging

En el caso de los logs tenemos varias opciones disponibles:

  • delete
  • read
  • write

Estas opciones son acumulables y tomarán el valor de true o false para la activación o desactivación respectivamente.

Retention Policy

Storage Analytics no elimina los logs ni las métricas de manera automática. Tanto es así que si sobrepasamos los 20TB disponibles para este servicio, el mismo dejará de escribir hasta que exista espacio libre disponible. Existen dos formas de eliminar estos valores: Haciendo peticiones de forma manual para eliminar el contenido que deseemos, lo cual es un gasto facturable, o bien a través de la configuración de Retention policy. Con esta política podremos liberar espacio indicando el número de días de antigüedad de los logs y registros. Es decir, todos aquellos logs y registros que tengan más de X días de antigüedad serán eliminados de manera automática sin coste alguno para nosotros. El número máximo de días de retención es de 365 días.

Un punto a tener en cuenta respecto a esta política es que no podemos habilitarla si el número de días es inferior a uno. De ser así nuestra petición será denegada. Del mismo modo, si el elemento Enabled tiene valor false y especificamos un número de días la petición tampoco sería satisfactoria.

Metrics

En el caso de las métricas las opciones se reducen a dos:

  • Enabled
  • Retention Policy

El primer error que se comete cuando intentamos habilitar el servicio de métricas es darle únicamente valor al elemento Enabled. Una petición con este único valor positivo será denegada. El motivo es que necesitamos agregar un nuevo elemento llamado IncludeAPIs y asignarle el valor a true, mostrado anteriormente tanto en el XML de ejemplo como en la clase Metrics que utilizamos para el mapeo. Inicialmente este valor es ignorado puesto que sólo es necesario cuando el elemento Enabled es igual a true. Dicho esto, el apartado de las métricas de la petición debería ser igual al siguiente:

<?xml version="1.0" encoding="utf-8"?>
<StorageServiceProperties>
  <Logging>
    <Version>1.0</Version>
    <Read>false</Read>
    <Write>false</Write>
    <Delete>false</Delete>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Logging>
  <Metrics>
    <Enabled>true</Enabled>
    <Version>1.0</Version>
    <IncludeAPIs>true</IncludeAPIs>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Metrics>
</StorageServiceProperties>

Al igual que para la configuración de Logging, también podemos especificar una política de retención para las métricas. El sistema es exactamente el mismo. De hecho, la clase de mapeo utilizada para ambos campos, Loggging y Metrics, es RetentionPolicy.

Una vez que hemos aclarado todos los conceptos, veamos cuál sería el método para tramitar la petición:

[...]
        public void SetSettings(StorageServiceProperties storageServiceProperties)
        {
            var request = MakeRequest("http://{0}.{1}.core.windows.net/?restype=service&comp=properties", storageServiceProperties.ServiceName);

            using (var memoryStream = new MemoryStream())
            {
                var xmlSettings = new XmlWriterSettings { Indent = true };
                var xmlWriter = XmlWriter.Create(memoryStream, xmlSettings);

                //XmlIgnores
                var overrides = new XmlAttributeOverrides();

                if (storageServiceProperties.Metrics.Enabled)
                {
                    overrides.Add(typeof(Metrics), "IncludeAPIs", new XmlAttributes { XmlIgnore = false });
                    storageServiceProperties.Metrics.IncludeAPIs = true;
                }

                var xmlSerializer = new XmlSerializer(typeof(StorageServiceProperties), overrides);

                var xmlSerializerNamespaces = new XmlSerializerNamespaces();
                xmlSerializerNamespaces.Add("", ""); //remove namespaces

                xmlSerializer.Serialize(xmlWriter, storageServiceProperties, xmlSerializerNamespaces);

                memoryStream.Seek(0, SeekOrigin.Begin); //Start at the beggining

                var xml = new XmlDocument();
                xml.Load(memoryStream);

                //Delete nodes which have zero
                var days = xml.GetElementsByTagName("Days");
                for (var i = 0; i < days.Count; i++)
                {
                    if (int.Parse(days[i].InnerText) == 0)
                    {
                        var retentionPolicy = (days[i]).ParentNode.FirstChild;
                        retentionPolicy.InnerText = "false";
                        days[i].ParentNode.RemoveChild(days[i]);
                        i--;
                    }
                }

                memoryStream.SetLength(0); //reusing memory stream instance

                xml.Save(memoryStream); //Save changes

                request.ContentLength = memoryStream.Length;

                //Sign Request
                if (storageServiceProperties.ServiceName == "table")
                    _credentials.SignRequestLite(request);
                else
                    _credentials.SignRequest(request);

                using (var stream = request.GetRequestStream())
                {
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    memoryStream.CopyTo(stream);
                }

                try
                {
                    using (var httpWebResponse = (HttpWebResponse)request.GetResponse())
                    {
                        var requestId = httpWebResponse.Headers["RequestIdHeaderName"];
                        Trace.TraceInformation("Request id:{0} Status: {1}", requestId, httpWebResponse.StatusCode);

                        if (HttpStatusCode.Accepted != httpWebResponse.StatusCode)
                            throw new Exception(string.Format("Request {0} failed! Status code: {1}", requestId, httpWebResponse.StatusCode));
                    }
                }
                catch (WebException ex)
                {
                    Trace.TraceError(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd());
                }
            }
        }

El método SetSettings va a ser el encargado de gestionar el contenido recibido en la clase StorageServiceProperties. El primer paso será recuperar una petición genérica a través del método auxiliar MakeRequest pero en este caso aceptando el valor por defecto para el parámetro action, el método PUT, ya que queremos actualizar contenido en el destino. Para poder declarar los valores que queremos configurar, será necesario generar un contenido XML en función a los valores recibidos como parámetros. Si aprovechamos al máximo el objeto StorageServiceProperties, utilizaremos XmlSerializer pero en esta ocasión para serializar los valores que contiene. El mismo podrá ser modificado si:

  1. Las métricas van a ser habilitadas: Por lo que se sobreescribirá el atributo [XmlIgnore] de IncludeAPIs y se le añadirá el valor true a la propiedad para que figure en el XML resultante.
  2. Si existe algún valor a cero en los elementos Days de Retention Policy, tanto de Loggging como de Metrics, serán eliminados del XML y se modificará el valor del elemeto Enabled a false.

Con el XML generado ya estamos listos para firmar la petición y copiar contenido del memoryStream en la petición creada al principio del código. Dentro del bloque try se procederá al envío de nuestra nueva configuración. Si la misma no finalizara con el código 202 podríamos capturar la respuesta y visualizar cuál es el motivo del rechazo.

Adjunto el proyecto por si fuera de utilidad 😀

¡Saludos!