Monitoring as Code на базе VictoriaMetrics и Grafana

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Приветствую всех любителей Infrastructure as Code.

Как я уже писал в предыдущей статье, я люблю заниматься автоматизацией инфраструктуры. Сегодня представляю вашему вниманию вариант построения GitOps для реализации подхода Monitoring as Code.

Немного контекста

Инфраструктура проекта, в котором я сейчас работаю, очень разнородна: k8s-кластера, отдельные docker-хосты с контейнерами, сервисы в обычных systemd-демонах и т.д. Кроме этого, у нас есть PROD, STAGE и DEV-окружения, которые с точки зрения архитектуры могут отличаться. Все эти окружения очень динамичны, постоянно деплоятся новые машины и удаляются старые. К слову, эту часть мы выполняем с помощью Terraform и Ansible (возможно расскажу подробнее в своей очередной статье). Для каждого окружения у нас используется своя инфраструктура мониторинга.

Исторически мы в проекте используем Prometheus-стек. Он отлично подходит для нашей динамической инфраструктуры. Если пройтись по отдельным компонентам, то получится следующий стандартный список компонентов:

  • Сбор и хранение метрик - Prometheus

  • Экспорт метрик - различные экспортеры (Node exporter, Postgres exporter, MongoDB exporter, ...).

  • Визуализация - Grafana

  • Алертинг - Alertmanager

В какой-то момент мы заменили Prometheus на VictoriaMetrics (кластерную версию), благодаря чему сэкономили кучу ресурсов и начали хранить наши метрики глубиной в 1 год. Если кто-то еще не знаком с этим замечательным продуктом, советую почитать про него. Мы мигрировали на него практически безболезненно, даже не меняя свои конфиги. В результате Prometheus у нас был заменен на несколько компонентов: vmagent + amalert + vmselect + vminsert + vmstorage.

Большинство из описанных в статье конфигураций подходят как для VictoriaMetrics, так и для Prometheus.

Этапы автоматизации мониторинга

1 этап. Исходное состояние, отсутствие автоматизации

Изначально изменения в конфигурацию Prometheus мы вносили вручную. В Prometheus не использовался никакой Service Discovery, использовался обычный static_config. И, как вы уже наверное догадались, очень быстро наш файл prometheus.yml превратился в портянку из 1000+ строк, которые могли содержать в себе какие-то старые закомментированные targets, лишние jobs и т.д. Почему? Потому что админы никогда не удаляют строки из конфигов, строки просто комментируются до лучших времен.

Читать этот файл из консоли linux было невозможно, внести изменения и не накосячить где-то по пути - просто нереально. Про формат конфигурации Prometheus подробнее можно почитать здесь.

Аналогичная ситуация была и с конфигурацией алертов prometheus, а также Alertmanager.

Дашборды Grafana редактировались также вручную и перетаскивались между несколькими инстансами Grafana через механизмы экспорта/импорта.

На данном этапе у нас не было никакой автоматизации, и, следовательно, тут мы мучались со следующими проблемами:

  • Создали новую машину, но забыли добавить в мониторинг. Когда понадобились метрики, вспомнили про эту машины.

  • Удалили машину, но забыли удалить из мониторинга. Заморгал алертинг, возбудилась группа дежурных (у нас и такое тоже есть).

  • Проводим работы на какой-либо машине, забыли заглушить для неё алерты. Дежурная смена опять звонит.

  • Внесли изменения изменения в дашборд Grafana в одном окружении, забыли перенести в другое окружение. В результате получаем разные дашборды в окружениях.

  • Конфигурация мониторинга является черным ящиком для всех, кто не имеет доступ к машине по ssh. У разработчиков часто возникают вопросы по метрикам, алертам и дашбордам.

  • Разработчики не могут внести изменения в дашборд, потому что у них права только Viewer.

  • И т.д.

2 этап. Хранение статической конфигурации в Git

Очень быстро мы намучились с ручной конфигурацией VictoriaMetrics и пришли к следующему варианту: решили хранить конфиги VictoriaMetrics и Alertmanager в Git. Доставка конфигурации пока выполнялась вручную (по факту - одна команда git pull).

Также мы переделали scrape-конфиги VictoriaMetrics в file_sd_config. Это не сильно упростило конфигурацию, но зато позволило структурировать её за счёт вынесения таргетов в отельные файлы.

С точки зрения автоматизации данный этап не сильно отличается от предыдущего, поскольку мы по-прежнему испытываем все проблемы, описанные выше. Но теперь мы хотя бы храним конфигурацию в Git и можем командно работать с ней.

Большинство из описанных выше проблем справедливы и для данного этапа, поэтому двигаемся дальше.

3 этап. GitOps для мониторинга

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

Сразу хочу обозначить, что в данный момент описанное решение находится в стадии тестирования и некоторые части могут измениться при вводе в продакшн.

Service Discovery

Вместо статической конфигурации мы решили использовать Service Discovery. У нас в инфраструктуре уже давно был Hashicorp Consul (в качестве KV-хранилища), но теперь мы решили его использовать как Service Discovery для мониторинга.

Для этого на каждую машину во всех наших окружениях мы установили consul-агент в режиме клиента. Через него мы начали регистрировать наши prometheus-экспортеры как сервисы в Consul. Делается это очень просто: в каталог конфигурации consul-агента необходимо подложить небольшой JSON-файл с минимальной информацией о сервисе. А затем сделать релоад сервиса consul на данном хосте, чтоб агент перечитал конфигурацию и отправил изменения в кластер. Подробнее о регистрации сервисов можно почитать в документации Consul.

Например, для Node Exporter файл может выглядеть следующим образом:

node_exporter.json
{
  "service": {
    "name": "node_exporter",
    "port": 9100,
    "meta": {
      "metrics_path": "/metrics",
      "metrics_scheme": "http"
    }
  }
}

Такой способ регистрации сервиса очень удобен, потому что всю работу за нас делает нативный consul-агент, от нас требуется лишь подложить в нужное место JSON-файл. При этом обновление и дерегистрация сервиса выполняется аналогичным образом (с помощью обновления или удаления файла).

Дерегистрация машин/сервисов (например, для удаления машины) может также производиться с помощью штатного выключения сервиса consul на машине. При остановке consul-агент выполняет graceful-shutdown, который выполняет дерегистрацию.

Кроме этого, дерегистрацию можно выполнить через Consul API.

VictoriaMetrics Configuration

Поскольку мы перешли на Service Discovery, теперь мы можем использовать consul_sd_config в нашем scrape-конфиге VictoriaMetrics. Таким образом, наш файл из 1000+ строк превратился в 30+ строк примерно следующего вида:

prometheus.yml
global:
  scrape_interval: 30s

scrape_configs:
  - job_name: exporters
    consul_sd_configs:
      - server: localhost:8500
    relabel_configs:
      # drop all targets that do not have metrics_path key in metadata
      - source_labels: [__meta_consul_service_metadata_metrics_path]
        regex: ^/.+$
        action: keep
      # set metrics path from metrics_path metadata key
      - source_labels: [__meta_consul_service_metadata_metrics_path]
        target_label: __metrics_path__
      # set metrics scheme from metrics_scheme metadata key
      - source_labels: [__meta_consul_service_metadata_metrics_scheme]
        regex: ^(http|https)$
        target_label: __scheme__
      - source_labels: [__meta_consul_dc]
        target_label: consul_dc
      - source_labels: [__meta_consul_health]
        target_label: consul_health
      - action: labelmap
        regex: __meta_consul_metadata_(.+)
        replacement: $1
      - source_labels: [__meta_consul_node]
        target_label: host
      - source_labels: [__meta_consul_node, __meta_consul_service_port]
        separator: ":"
        target_label: instance
      - source_labels: [__meta_consul_service]
        target_label: job
      - source_labels: [__meta_consul_node, __meta_consul_service_port]
        separator: ":"
        target_label: __address__

Такая конфигурация заставляет Prometheus брать список хостов из Consul Service Discovery. Т.е. если хост добавился в Consul, то он через несколько секунд появляется в Prometheus.

С помощью relabel_config мы можем делать любые преобразования данных, полученных из Consul в лейблы Prometheus. Например, мы через метаданные сервиса Consul передаем схему (http или https) и путь к метрикам экспортера (обычно /metrics, но бывает и другой).

Также с помощью метаданных и тегов consul, мы можем фильтровать хосты, которые будут добавлены в Prometheus (при условии, что эти теги или метаданные мы добавили в конфигурацию Сonsul при регистрации сервиса). Например, вот так мы можем брать только хосты из DEV-окружения:

prometheus.yml
scrape_configs:
  - job_name: exporters
    consul_sd_configs:
      - server: localhost:8500
        tags:
          - dev
    relabel_configs:
    ...

При использовании Consul Service Discovery мы можем также получать статус хоста (метка __meta_consul_health). С помощью данного поля мы можем выводить наши хосты в Maintenance-режим. Для этого у агента Consul есть специальная команда maint.

Примеры команд
# включение maintenance-режима для хоста
consul maint -enable
# включение maintenance-режима для отдельного сервиса
consul maint -service=node_exporter -enable
# выключение maintenance-режима для хоста
consul maint -disable

С помощью этой метки мы может обрабатывать событие вывода хостов на обслуживание и не создавать лишние алерты. Для этого необходимо заранее предусмотреть данное исключение в своих правилах алертинга.

Grafana Provisioning

Если вы работали с Grafana, то Вы, наверное, уже знаете, что каждый дашборд представляет собой JSON-файл. Также у Grafana есть API, через который можно пропихивать эти дашборды.

Кроме этого, есть специальный механизм Grafana Provisioning, который позволяет вообще всю конфигурацию Grafana хранить в виде файлов в формате YAML. Этот механизм работает следующим образом:

  1. Мы пишем конфигурацию наших data sources, plugins, dashboards и складываем её в определенный каталог.

  2. Grafana при старте создает все описанные в YAML объекты и импортирует дашборды из указанного каталога.

При импорте дашбордов есть следующие возможности:

  • Grafana может импортировать структуру каталогов и создать их у себя в UI. Импортированные дашборды будут разложены по каталогам в соответствии с расположением JSON-файлов.

  • После импорта дашборды можно сделать нередактируемыми через UI (актуально, если планируете вносить все изменения только через код).

  • Для дашбордов можно задать статические uid, чтоб определить зафиксировать ссылки на получившиеся дашборды.

  • Grafana умеет перечитывать содержимое каталога и применять изменения в дашбордах.

  • Если JSON-файл исчез из каталога, Grafana может соответственно убирать его из UI.

Пример конфигурации Data Sources:

datasources.yml
# config file version
apiVersion: 1

# list of datasources that should be deleted from the database
deleteDatasources: []

# list of datasources to insert/update depending
# what's available in the database
datasources:
  # <string, required> name of the datasource. Required
  - name: VictoriaMetrics
    # <string, required> datasource type. Required
    type: prometheus
    # <string, required> access mode. proxy or direct (Server or Browser in the UI). Required
    access: proxy
    # <int> org id. will default to orgId 1 if not specified
    orgId: 1
    # <string> custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically
    uid: victoria_metrics
    # <string> url
    url: http://my.victoria.metrics:8481/select/0/prometheus
    # <string> Deprecated, use secureJsonData.password
    password:
    # <string> database user, if used
    user:
    # <string> database name, if used
    database:
    # <bool> enable/disable basic auth
    basicAuth:
    # <string> basic auth username
    basicAuthUser:
    # <string> Deprecated, use secureJsonData.basicAuthPassword
    basicAuthPassword:
    # <bool> enable/disable with credentials headers
    withCredentials:
    # <bool> mark as default datasource. Max one per org
    isDefault: true
    # <map> fields that will be converted to json and stored in jsonData
    jsonData:
    # <string> json object of data that will be encrypted.
    secureJsonData:
    # datasource version
    version: 1
    # <bool> allow users to edit datasources from the UI.
    editable: false
dashboards.yml
apiVersion: 1

providers:
  # <string> an unique provider name. Required
  - name: dashboards
    # <int> Org id. Default to 1
    orgId: 1
    # <string> name of the dashboard folder.
    folder: ''
    # <string> folder UID. will be automatically generated if not specified
    folderUid: ''
    # <string> provider type. Default to 'file'
    type: file
    # <bool> disable dashboard deletion
    disableDeletion: false
    # <int> how often Grafana will scan for changed dashboards
    updateIntervalSeconds: 10
    # <bool> allow updating provisioned dashboards from the UI
    allowUiUpdates: false
    options:
      # <string, required> path to dashboard files on disk. Required when using the 'file' type
      path: /var/lib/grafana/dashboards
      # <bool> use folder names from filesystem to create folders in Grafana
      foldersFromFilesStructure: true

Согласно нашей конфигурации Grafana должна создать Data Source типа prometheus с URL http://my.victoria.metrics:8481/select/0/prometheus. Также из каталога /var/lib/grafana/dashboards должны быть импортированы каталоги и дашборды.

Таким образом, мы получаем полностью определяемое состояние Grafana из кода.

Dashboards as Code

Перейдем к самим JSON-файлам дашбордов. Те, кто видел эти JSON-ы, справедливо сделают замечание о том, что формировать и поддерживать их вручную (без Grafana UI) невозможно. С этим я соглашусь, но к счастью, для этого создали специальный фреймворк grafonnet-lib, который позволяет писать дашборды с использованием языка Jsonnet.

Указанный фреймворк уже содержит набор функций, с помощью которых можно формировать панели для дашбордов. Язык Jsonnet также позволяет писать собственные функции, а также структурировать код, раскладывая его по отдельным файлам и каталогам.

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

GitOps

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

Мы давно у себя используем Gitlab для хранения наших инфраструктурных репозиториев, а также Gitlab CI для CI/CD.

Собрав всё в кучу, мы получили следующую структуру каталогов.

  • /сi - файлы, используемые в Gitlab CI

  • /grafonnet-lib - git submodule для исходников фреймворка grafonnet-lib

  • /dev - конфигурация мониторинга DEV-окружения

  • /stage - то же, самое для STAGE

  • /prod - то же самое для PROD

  • /tests - файлы для тестирования дашбордов (например docker-compose для запуска Grafana)

Каждый из каталогов dev, stage, prod в свою очередь содержит следующий набор каталогов:

  • alertmanager

  • blackbox

  • grafana

  • vmagent

  • vmalert

В указанных каталогах хранится конфигурация соответствующих компонентов системы мониторинга. В каталоге grafana, кроме конфигурации Provisioning, хранятся также исходники дашбордов на языке jsonnet, которые компилируются в JSON-файлы в процессе деплоя в Gitlab CI.

Конфигурация Gitlab CI у нас выглядит следующим образом:

.gitlab-ci.yml
variables:
  CA_CERT_FILE: /etc/gitlab-runner/certs/ca.crt
  VMETRICS_IMAGE: $CI_REGISTRY_IMAGE/vm:ci-0.0.5
  JSONNET_IMAGE: $CI_REGISTRY_IMAGE/jsonnet:ci-0.0.5
  YAMLLINT_IMAGE: cytopia/yamllint:1.26
  RSYNC_IMAGE: instrumentisto/rsync-ssh:alpine3.14

  # can be overrided in project CI/CD settings
  GRAFANA_USER: admin
  GRAFANA_PASSWORD: admin

  # should be defined in project CI/CD settings
  #SSH_PRIVATE_KEY_FILE:
  #SSH_USERNAME:

include:
  - local: '*/.gitlab-ci.yml'

stages:
  - build_image
  - validate
  - build
  # - review
  - deploy

build_image:
  before_script: []
  stage: build_image
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  variables:
    REGISTRY_TAG: $CI_COMMIT_TAG
    CONTEXT_DIR: $CI_PROJECT_DIR/ci/$IMAGE_NAME
  script:
    - mkdir -p /kaniko/.docker
    - cat $CA_CERT_FILE >> /kaniko/ssl/certs/additional-ca-cert-bundle.crt
    - cp -L $CA_CERT_FILE $CONTEXT_DIR/ca.crt
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor --context $CONTEXT_DIR --dockerfile $CONTEXT_DIR/Dockerfile $BUILD_ARGS --destination $CI_REGISTRY_IMAGE/$IMAGE_NAME:$REGISTRY_TAG
  cache: {}
  rules:
    - if: '$CI_COMMIT_TAG =~ /^ci-.*/'
  parallel:
    matrix:
      - IMAGE_NAME: jsonnet
        GO_JSONNET_VERSION: '0.17.0'
        BUILD_ARGS: '--build-arg GO_JSONNET_VERSION'
      - IMAGE_NAME: vm
        VMETRICS_TAG: 'v1.62.0'
        ALERTMANAGER_TAG: 'v0.22.2'
        BLACKBOX_TAG: 'v0.19.0'
        BUILD_ARGS: '--build-arg VM_VERSION --build-arg ALERTMANAGER_TAG --build-arg BLACKBOX_TAG'

.yamllint:
  stage: validate
  image:
    name: $YAMLLINT_IMAGE
    entrypoint: [""]
  script:
    - yamllint ${WORK_DIR}/ -c .yamllint.yml

.jsonnetlint:
  stage: validate
  image: $JSONNET_IMAGE
  variables:
    GIT_SUBMODULE_STRATEGY: recursive
    JSONNET_PATH: $CI_PROJECT_DIR/grafonnet-lib
  script:
    - cd $WORK_DIR/grafana/grafonnet
    - if [[ -f "dashboards.jsonnet" ]]; then jsonnetfmt --test $(find . -name '*.jsonnet'); fi
    - if [[ -f "dashboards.jsonnet" ]]; then jsonnet-lint dashboards.jsonnet; fi

.config_validate:
  stage: validate
  image: $VMETRICS_IMAGE
  script:
    - vmalert -dryRun -rule ${WORK_DIR}/vmalert/*.yml
    - vmagent -dryRun -promscrape.config ${WORK_DIR}/vmagent/scrape_config.yml -promscrape.config.strictParse
    - blackbox_exporter --config.check --config.file=${WORK_DIR}/blackbox/blackbox.yml
    - amtool check-config ${WORK_DIR}/alertmanager/

.build:
  stage: build
  image: $JSONNET_IMAGE
  variables:
    GIT_SUBMODULE_STRATEGY: recursive
    JSONNET_PATH: $CI_PROJECT_DIR/grafonnet-lib
  script:
    - cd ${WORK_DIR}/grafana/grafonnet
    - jsonnet -m dashboards -c -V dasboardEditable=false dashboards.jsonnet
  artifacts:
    paths:
      - ${WORK_DIR}/grafana/grafonnet/dashboards

.deploy:
  stage: deploy
  image: $RSYNC_IMAGE
  variables:
    SSH_CONFIG: |
      Host *
        StrictHostKeyChecking no
        UserKnownHostsFile=/dev/null
        LogLevel ERROR
    SSH_SERVER: $SSH_USERNAME@$MON_SERVER
    GRAFANA_PORT: 3000
    GRAFANA_SCHEME: http
  environment:
    name: $WORK_DIR
  script:
    - set -x
    - eval $(ssh-agent -s)
    - mkdir ~/.ssh/
    - chmod 700 ~/.ssh
    - echo "$SSH_CONFIG" > ~/.ssh/config
    - cat $SSH_PRIVATE_KEY_FILE | tr -d '\r' | ssh-add - > /dev/null
    - alias rsync="rsync -ai --delete --no-perms --no-owner --no-group --rsync-path='sudo rsync' --timeout=15"
    - RSYNC_OUT=$(rsync ${WORK_DIR}/vmagent $SSH_SERVER:/opt/vm/)
    - |
        if [ -n "$RSYNC_OUT" ]; then
          ssh $SSH_SERVER "sudo chown -R vmcluster:vmcluster /opt/vm/vmagent/* && sudo systemctl reload vmagent.service"
        fi
    - RSYNC_OUT=$(rsync ${WORK_DIR}/vmalert $SSH_SERVER:/opt/vm/)
    - |
        if [ -n "$RSYNC_OUT" ]; then
          ssh $SSH_SERVER "sudo chown -R vmcluster:vmcluster /opt/vm/vmalert/* && sudo systemctl reload vmalert.service"
        fi
    - RSYNC_OUT=$(rsync ${WORK_DIR}/blackbox/blackbox.yml $SSH_SERVER:/opt/blackbox_exporter/)
    - |
        if [ -n "$RSYNC_OUT" ]; then
          ssh $SSH_SERVER "sudo chown -R blackbox_exporter:blackbox_exporter /opt/blackbox_exporter/blackbox.yml && sudo systemctl reload blackbox_exporter.service"
        fi
    - rsync ${WORK_DIR}/grafana/provisioning $SSH_SERVER:/etc/grafana/
    - ssh $SSH_SERVER "sudo chown -R root:grafana /etc/grafana/provisioning"
    - rsync ${WORK_DIR}/grafana/grafonnet/dashboards $SSH_SERVER:/var/lib/grafana/
    - ssh $SSH_SERVER "sudo chown -R grafana:grafana /var/lib/grafana/dashboards"
    - ssh $SSH_SERVER "curl -X POST -sSf $GRAFANA_SCHEME://$GRAFANA_USER:$GRAFANA_PASSWORD@localhost:$GRAFANA_PORT/api/admin/provisioning/dashboards/reload"
    - ssh $SSH_SERVER "curl -X POST -sSf $GRAFANA_SCHEME://$GRAFANA_USER:$GRAFANA_PASSWORD@localhost:$GRAFANA_PORT/api/admin/provisioning/datasources/reload"

Какие действия мы выполняем в CI/CD:

  1. Валидация всех файлов конфигурации (yamllint + check конфигов всех компонентов)

  2. Компиляция дашбордов Grafana

  3. Деплой всей конфигурации на сервер мониторинга (также можно использовать несколько инстансов, объединенных в кластер).

Для деплоя мы используем обычный rsync с набором необходимых ключей (например, для удаления лишних файлов на сервере назначения).

Для локальной разработки мы используем скрипт, который компилирует дашборды и запускает Grafana в docker-compose. Разработчик дашборда может сразу увидеть внесенные изменения.

Заключение

В данной статье описаны этапы автоматизации системы мониторинга на базе Prometheus и Grafana. Используемые подходы позволяют решить ряд задач:

  • Используя Service Discovery, мы получаем полную автоматизацию добавления и удаления хостов в мониторинг. Т.е. новые машины встают на мониторинг сразу после деплоя. Для удаления машин с мониторинга, можно использовать любые механизмы (наприме, можно использовать Destroy-Time Provisioners для Terraform, который будет выполнять дерегистрацию сервиса в Consul)

  • С помощью maintenance-режима мы можем выводить хосты на обслуживание и не получать при этом лишних алертов. Дежурная смена может спать спокойно :)

  • Используя подход Grafana as Code, мы получаем полностью детерминированное состояние наших дашбордов. При внесении изменений в конфигурацию Prometheus, мы сразу вносим изменения в дашборды.

  • Используя Gitlab CI, мы выстраиваем процесс GitOps для нашей системы мониторинга. Т.е. Git становится единым источником правды для всей системы мониторинга. Больше не требуется никаких ручных кликов в Grafana UI и никакой правки файлов конфигурации в консоли Linux.

  • И самое главное: теперь наши разработчики могут приходить в этот репозиторий, вносить изменения и присылать Pull Request.

Всем спасибо за внимание! Буду рад ответить на любые вопросы касательно данной темы.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Какой стек вы используете для мониторинга?

  • 100,0%Zabbix1
  • 0,0%Prometheus0
  • 0,0%Netdata0
  • 0,0%Nagios0
  • 0,0%другое0

Как вы управляете конфигурацией?

  • 0,0%Ручное редактирование конфигурационных файлов0
  • 100,0%Web UI1
  • 0,0%GitOps0
  • 0,0%другое0
Источник: https://habr.com/ru/post/568090/


Интересные статьи

Интересные статьи

Всем привет. Меня зовут Стас, в компании Домклик я курирую разработку сервисов бек-офиса для ипотечного кредитования Сбербанка. В последнее время во всевозможных докладах и под...
Перевод с сайта Electric DIY Lab Всем привет, представляю вам изготовленную мною машину для намотки тороидальных катушек на базе Arduino. Машина автоматически наматывает провол...
Пока писал эту сугубо техническую статью, Хабр успел превратиться в местное отделение ВОЗ и теперь мне даже стыдно ее публиковать… но в душе теплится надежда, что айтишники еще не разбежались и о...
Развивая любой продукт, будь то видеосервис или лента, истории или статьи, хочется уметь измерять условное «счастье» пользователя. Понимать, делаем мы своими изменениями лучше или хуже, корре...
Довольно часто владельцы сайтов просят поставить на свои проекты индикаторы курсов валют и их динамику. Можно воспользоваться готовыми информерами, но они не всегда позволяют должным образом настроить...