Дашборды в стиле Kibana для Cloudwatch

Вопрос или проблема

Мы активно используем ELK stack и используем Kibana для исследовательского анализа наших логов. Мы ищем возможность перейти с Elasticsearch на Cloudwatch в качестве хранилища для наших операционных логов. Существует ли какое-либо программное обеспечение, похожее на Kibana, для Cloudwatch?

Я знаю, что есть Grafana, и мы активно её используем, но есть случаи, когда Kibana превосходит Grafana, и многие наши разработчики предпочитают Kibana.

Я предпочел бы что-то, что можно разместить на собственном сервере, но готов рассмотреть облачное/SaaS решение, если оно того стоит.

Amazon предлагает сервис Elasticsearch, который включает Kibana. Настройка ES автоматически предоставляет вам Kibana. Вам это не понравится. Очень ограничено. Но – Есть способ получить логи CloudWatch в ES. Ниже приведен код, установленный как функция Lambda, которая отправляет данные в Amazon, размещенный ES. Вы можете сделать то же самое для вашего собственного экземпляра EC2 с установленным ES.

введите описание изображения здесь

    // v1.1.2
var https = require('https');
var zlib = require('zlib');
var crypto = require('crypto');

var endpoint="search-edge-logs-73mf7h7wbvf6pjhsxexqksba6q.us-west-2.es.amazonaws.com";

exports.handler = function(input, context) {
    // декодируем ввод из base64
    var zippedInput = new Buffer(input.awslogs.data, 'base64');

    // разжимаем ввод
    zlib.gunzip(zippedInput, function(error, buffer) {
        if (error) { context.fail(error); return; }

        // парсим ввод из JSON
        var awslogsData = JSON.parse(buffer.toString('utf8'));

        // преобразуем ввод в документы Elasticsearch
        var elasticsearchBulkData = transform(awslogsData);

        // пропускаем контрольные сообщения
        if (!elasticsearchBulkData) {
            console.log('Получено контрольное сообщение');
            context.succeed('Контрольное сообщение успешно обработано');
            return;
        }

        // отправляем документы в сервис Amazon Elasticsearch
        post(elasticsearchBulkData, function(error, success, statusCode, failedItems) {
            console.log('Ответ: ' + JSON.stringify({ 
                "statusCode": statusCode 
            }));

            if (error) { 
                console.log('Ошибка: ' + JSON.stringify(error, null, 2));

                if (failedItems && failedItems.length > 0) {
                    console.log("Неудачные элементы: " +
                        JSON.stringify(failedItems, null, 2));
                }

                context.fail(JSON.stringify(error));
            } else {
                console.log('Успешно: ' + JSON.stringify(success));
                context.succeed('Успешно');
            }
        });
    });
};

function transform(payload) {
    if (payload.messageType === 'CONTROL_MESSAGE') {
        return null;
    }

    var bulkRequestBody = '';

    payload.logEvents.forEach(function(logEvent) {
        var timestamp = new Date(1 * logEvent.timestamp);

        var indexPrefix = payload.logGroup

        // формат имени индекса: cwl-YYYY.MM.DD
        var indexName = [
            indexPrefix + timestamp.getUTCFullYear(),              // год
            ('0' + (timestamp.getUTCMonth() + 1)).slice(-2),  // месяц
            ('0' + timestamp.getUTCDate()).slice(-2)          // день
        ].join('.');

        console.log('Индекс установлен на ' + indexName);

        var source = buildSource(logEvent.message, logEvent.extractedFields);
        source['@id'] = logEvent.id;
        source['@timestamp'] = new Date(1 * logEvent.timestamp).toISOString();
        source['@message'] = logEvent.message;
        source['@owner'] = payload.owner;
        source['@log_group'] = payload.logGroup;
        source['@log_stream'] = payload.logStream;

        var action = { "index": {} };
        action.index._index = indexName;
        action.index._type = payload.logGroup;
        action.index._id = logEvent.id;

        bulkRequestBody += [ 
            JSON.stringify(action), 
            JSON.stringify(source),
        ].join('\n') + '\n';
    });
    return bulkRequestBody;
}

function buildSource(message, extractedFields) {
    if (extractedFields) {
        var source = {};

        for (var key in extractedFields) {
            if (extractedFields.hasOwnProperty(key) && extractedFields[key]) {
                var value = extractedFields[key];

                if (isNumeric(value)) {
                    source[key] = 1 * value;
                    continue;
                }

                jsonSubString = extractJson(value);
                if (jsonSubString !== null) {
                    source['$' + key] = JSON.parse(jsonSubString);
                }

                source[key] = value;
            }
        }
        return source;
    }

    jsonSubString = extractJson(message);
    if (jsonSubString !== null) { 
        return JSON.parse(jsonSubString); 
    }

    return {};
}

function extractJson(message) {
    var jsonStart = message.indexOf('{');
    if (jsonStart < 0) return null;
    var jsonSubString = message.substring(jsonStart);
    return isValidJson(jsonSubString) ? jsonSubString : null;
}

function isValidJson(message) {
    try {
        JSON.parse(message);
    } catch (e) { return false; }
    return true;
}

function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
}

function post(body, callback) {
    var requestParams = buildRequest(endpoint, body);

    var request = https.request(requestParams, function(response) {
        var responseBody = '';
        response.on('data', function(chunk) {
            responseBody += chunk;
        });
        response.on('end', function() {
            var info = JSON.parse(responseBody);
            var failedItems;
            var success;

            if (response.statusCode >= 200 && response.statusCode < 299) {
                failedItems = info.items.filter(function(x) {
                    return x.index.status >= 300;
                });

                success = { 
                    "attemptedItems": info.items.length,
                    "successfulItems": info.items.length - failedItems.length,
                    "failedItems": failedItems.length
                };
            }

            var error = response.statusCode !== 200 || info.errors === true ? {
                "statusCode": response.statusCode,
                "responseBody": responseBody
            } : null;

            callback(error, success, response.statusCode, failedItems);
        });
    }).on('error', function(e) {
        callback(e);
    });
    request.end(requestParams.body);
}

function buildRequest(endpoint, body) {
    var endpointParts = endpoint.match(/^([^\.]+)\.?([^\.]*)\.?([^\.]*)\.amazonaws\.com$/);
    var region = endpointParts[2];
    var service = endpointParts[3];
    var datetime = (new Date()).toISOString().replace(/[:\-]|\.\d{3}/g, '');
    var date = datetime.substr(0, 8);
    var kDate = hmac('AWS4' + process.env.AWS_SECRET_ACCESS_KEY, date);
    var kRegion = hmac(kDate, region);
    var kService = hmac(kRegion, service);
    var kSigning = hmac(kService, 'aws4_request');

    var request = {
        host: endpoint,
        method: 'POST',
        path: '/_bulk',
        body: body,
        headers: { 
            'Content-Type': 'application/json',
            'Host': endpoint,
            'Content-Length': Buffer.byteLength(body),
            'X-Amz-Security-Token': process.env.AWS_SESSION_TOKEN,
            'X-Amz-Date': datetime
        }
    };

    var canonicalHeaders = Object.keys(request.headers)
        .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1; })
        .map(function(k) { return k.toLowerCase() + ':' + request.headers[k]; })
        .join('\n');

    var signedHeaders = Object.keys(request.headers)
        .map(function(k) { return k.toLowerCase(); })
        .sort()
        .join(';');

    var canonicalString = [
        request.method,
        request.path, '',
        canonicalHeaders, '',
        signedHeaders,
        hash(request.body, 'hex'),
    ].join('\n');

    var credentialString = [ date, region, service, 'aws4_request' ].join("/");

    var stringToSign = [
        'AWS4-HMAC-SHA256',
        datetime,
        credentialString,
        hash(canonicalString, 'hex')
    ] .join('\n');

    request.headers.Authorization = [
        'AWS4-HMAC-SHA256 Credential=" + process.env.AWS_ACCESS_KEY_ID + "/" + credentialString,
        'SignedHeaders=" + signedHeaders,
        "Signature=" + hmac(kSigning, stringToSign, "hex')
    ].join(', ');

    return request;
}

function hmac(key, str, encoding) {
    return crypto.createHmac('sha256', key).update(str, 'utf8').digest(encoding);
}

function hash(str, encoding) {
    return crypto.createHash('sha256').update(str, 'utf8').digest(encoding);
}

Ответ или решение

Альтернативы Kibana для анализа логов CloudWatch

Вопрос о том, как найти подходящее решение для анализа логов в Amazon CloudWatch, с учётом предпочтений пользователей Kibana, является актуальным для многих команд, использующих ELK-стек. Kibana привлекает разработчиков своей продуманной визуализацией данных, хотя Grafana также заслуживает внимания. Давайте рассмотрим имеющиеся альтернативы, их возможности и подходы к интеграции с CloudWatch.

1. Варианты программного обеспечения для визуализации

Amazon Managed Grafana: Хотя вы уже используете Grafana, стоит упомянуть, что Amazon предлагает управляемую версию этой платформы. Она позволяет легко интегрироваться с различными источниками данных, включая CloudWatch. Grafana предоставляет множество плагинов и визуальных компонентов, но изначально она не была разработана для глубокого поиска и анализа логов, что делает её менее гибкой по сравнению с Kibana.

OpenSearch Dashboards: OpenSearch — это проект с открытым исходным кодом, который возник после разделения ElasticSearch. OpenSearch включает в себя Visual Dashboard, аналогичный Kibana. Данная платформа позволяет обрабатывать логи и данные, а также имеет структуру плагинов для расширения функциональности. Она может быть развернута на собственных серверах или использоваться в виде управляемого сервиса.

Grafana Loki: Хотя вы уже знакомы с Grafana, стоит отметить, что Loki представляет собой систему для сбора логов с интеграцией в Grafana. Это решение может быть совместимо с привычной экосистемой Grafana и работает с временными метками, упрощая анализ логов. Для пользователей, уже работающих с Grafana, использование Loki может быть удобным решением.

2. Интеграция CloudWatch с Elasticsearch

Поскольку вы всё же рассматриваете возможность ведения логов в Elasticsearch, стоит упомянуть, что вы можете настроить процесс передачи логов из CloudWatch в Elasticsearch. Для этого, как указано в предоставленном коде Lambda, можно использовать AWS Lambda для обработки логов и их последующей отправки в Elasticsearch.

Процесс выглядит так:

  • Логи CloudWatch активируются через AWS Lambda.
  • Lambda функция обрабатывает сжатые данные в формате JSON.
  • Далее происходит преобразование данных в соответствующий формат для записи в Elasticsearch.
  • В завершение данные отправляются в выбранный кластер Elasticsearch.

Эти шаги обеспечивают получение всей необходимой информации в реальном времени и позволяют использовать возможности Kibana для анализа.

3. Открытые решения

Если вы хотите использовать решение на основе открытого кода, рассмотрите использование таких инструментов, как Prometheus и Loki. Комплексное использование этих двух инструментов с Grafana предоставляет возможность визуализировать не только метрики, но и логи, что может быть полезным для вашей команды.

Заключение

Подводя итог, вы можете выбрать, исходя из предпочтений своих разработчиков и потребностей вашей команды, между уже упомянутыми решениями. OpenSearch Dashboards может стать наиболее близким аналогом Kibana с точки зрения функциональности, в то время как интеграция с Elasticsearch через AWS Lambda предоставит вам гибкость и расширенные возможности анализа логов.

Предоставленный план по интеграции и ряд альтернатив помогут вашей команде найти эффективное решение, которое сможет удовлетворить ваши требования к анализу данных в CloudWatch.

Оцените материал
Добавить комментарий

Капча загружается...