воскресенье, 20 января 2019 г.

Кастомный Health-индикатор в Spring Boot Actuator

Для отслеживания параметров работы приложения удобно использовать Spring Boot Actuator. Он предоставляет такие возможности, как:
- создание своих (кастомных) метрик, индикаторов и счётчиков;
- экспорт метрик в различные агрегаторы и визуализаторы, включая InfluxDB и Graphite.

На одном из проектов мне необходимо было добавить возможность просмотра в заданном  формате определённых метрик микросервиса по HTTP-запросу установленного формата и выключение (полную остановку) сервиса по HTTP-запросу.

Задача интересная. И вот, как мы можем её решить:

1. Добавляем в pom.xml зависимости.

<dependencies>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
</dependencies>

2. Добавляем в application.properties:

# включаем отображение в Web всех метрик
management.endpoints.web.exposure.include=*

# активируем возможность выключения сервиса по HTTP-запросу
management.endpoint.shutdown.enabled=true

# активируем показ в Health-метриках дополнительных деталей (состяние баз данных, очередей и прочего)
management.endpoint.health.show-details=always

# изменяем стандартный путь к метрикам актуатора
management.endpoints.web.base-path=/api

# изменяем стандартный путь к Health-метрикам актуатора
management.endpoints.web.path-mapping.health=status

# задаём имя сервера
server.name=defaultName-123

3. Расширяем возможности Health-индикатора актуатора.

@Service
public class StatusEndpointService implements HealthIndicator {

    private MetricsEndpoint metricsEndpoint;
    @Value("${server.name}")
    private String serverName;

    @Autowired
    public void setMetricsEndpoint(MetricsEndpoint metricsEndpoint) {
        this.metricsEndpoint = metricsEndpoint;
    }

    @Override
    public Health health() {
        Map<String, Object> statusMap = new HashMap<>();

        MetricResponse response = metricsEndpoint.metric("process.uptime", null);
        long uptimeMiliseconds = (long) (response.getMeasurements().get(0).getValue() * 1000);
        long millis = uptimeMiliseconds % 1000;
        long second = (uptimeMiliseconds / 1000) % 60;
        long minute = (uptimeMiliseconds / (1000 * 60)) % 60;
        long hour = (uptimeMiliseconds / (1000 * 60 * 60)) % 24;
        long days = uptimeMiliseconds / (1000 * 60 * 60 * 24);
        String formatedUptime = String.format("%d.%02d:%02d:%02d.%03d", days, hour, minute, second, millis);

        statusMap.put("serverName", serverName);
        statusMap.put("uptime", formatedUptime);
        return Health.up().withDetails(statusMap).build();
    }
}

В данном случае я добавил отображение в деталях Health-метрик следующих сведений:
- имя сервера, на котором запущено приложение;
- время работы сервиса с момента запуска.

Состав сведений, которые мы можем добавить для отображения в Health-метриках, окраничивается только нашей фантазией.

Запустим сервис и понаправляем на него запросы.
Посмотрим Health-статус:

$ curl http://localhost:8080/api/status | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   231    0   231    0     0  46200      0 --:--:-- --:--:-- --:--:-- 46200
{
  "status": "UP",
  "details": {
    "statusEndpointService": {
      "status": "UP",
      "details": {
        "serverName": "localhost",
        "uptime": "0.00:07:29.735"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 354868289536,
        "free": 12128550912,
        "threshold": 10485760
      }
    }
  }
}

Посмотрим API метрик:

$ curl http://localhost:8080/api | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1838    0  1838    0     0   358k      0 --:--:-- --:--:-- --:--:--  358k
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/api",
      "templated": false
    },
    "auditevents": {
      "href": "http://localhost:8080/api/auditevents",
      "templated": false
    },
    "beans": {
      "href": "http://localhost:8080/api/beans",
      "templated": false
    },
    "caches": {
      "href": "http://localhost:8080/api/caches",
      "templated": false
    },
    "caches-cache": {
      "href": "http://localhost:8080/api/caches/{cache}",
      "templated": true
    },
    "health": {
      "href": "http://localhost:8080/api/status",
      "templated": false
    },
    "health-component-instance": {
      "href": "http://localhost:8080/api/status/{component}/{instance}",
      "templated": true
    },
    "health-component": {
      "href": "http://localhost:8080/api/status/{component}",
      "templated": true
    },
    "conditions": {
      "href": "http://localhost:8080/api/conditions",
      "templated": false
    },
    "shutdown": {
      "href": "http://localhost:8080/api/shutdown",
      "templated": false
    },
    "configprops": {
      "href": "http://localhost:8080/api/configprops",
      "templated": false
    },
    "env-toMatch": {
      "href": "http://localhost:8080/api/env/{toMatch}",
      "templated": true
    },
    "env": {
      "href": "http://localhost:8080/api/env",
      "templated": false
    },
    "info": {
      "href": "http://localhost:8080/api/info",
      "templated": false
    },
    "loggers": {
      "href": "http://localhost:8080/api/loggers",
      "templated": false
    },
    "loggers-name": {
      "href": "http://localhost:8080/api/loggers/{name}",
      "templated": true
    },
    "heapdump": {
      "href": "http://localhost:8080/api/heapdump",
      "templated": false
    },
    "threaddump": {
      "href": "http://localhost:8080/api/threaddump",
      "templated": false
    },
    "metrics-requiredMetricName": {
      "href": "http://localhost:8080/api/metrics/{requiredMetricName}",
      "templated": true
    },
    "metrics": {
      "href": "http://localhost:8080/api/metrics",
      "templated": false
    },
    "scheduledtasks": {
      "href": "http://localhost:8080/api/scheduledtasks",
      "templated": false
    },
    "httptrace": {
      "href": "http://localhost:8080/api/httptrace",
      "templated": false
    },
    "mappings": {
      "href": "http://localhost:8080/api/mappings",
      "templated": false
    }
  }
}

Остановим сервис:

$ curl -X POST localhost:8080/api/shutdown | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    35    0    35    0     0    324      0 --:--:-- --:--:-- --:--:--   324
{
  "message": "Shutting down, bye..."
}

Весь код на GitHub

суббота, 19 января 2019 г.

Логирование в Spring Boot с глобальным traceId (применяем Mapped Diagnostic Context (MDC))

В предыдущей статье я поверхностно описал о том, как и для чего можно использовать глобальные trace ID, внедрённые с помощью механизма, предоставленного библиотекой Spring Cloud Sleuth.

Сегодня хочу рассмотреть, как можно использовать trace ID с помощью Mapped Diagnostic Context (MDC), поддерживаемого Logback и Log4J. Для разнообразия, добавим в логирование также IP клиента, направившего REST-запрос.

1. Подключаем в pom.xml зависимости:

<dependencies>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
 </dependency>

 <dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
 </dependency>
</dependencies>

2. Добавляем файл конфигурации logback-spring.xml:

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <property name="LOG_FILE" value="log/demo.mdc"/><property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%-5level [%X{Slf4jMDCFilter.ID}] [%X{Slf4jMDCFilter.ClientIP}]) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <property name="FILE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{Slf4jMDCFilter.ID} %X[Slf4jMDCFilter.ClientIP]] ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" />

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${LOG_FILE}.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.log</FileNamePattern>
            <MaxHistory>7</MaxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>

3. Делаем класс конфигурации, который создаст FilterRegistrationBean для сервлета, и задаём имена HTTP заголовков и ключей MDC:

@Data
@Configuration
@ConfigurationProperties(prefix = slf4jfilter")
public class Slf4jMDCFilterConfiguration {

    public static final String DEFAULT_RESPONSE_TOKEN_HEADER = "X-Token";
    public static final String DEFAULT_REQUEST_TOKEN_HEADER = "X-Token";
    public static final String DEFAULT_MDC_ID_TOKEN_KEY = "Slf4jMDCFilter.ID";
    public static final String DEFAULT_MDC_CLIENT_IP_KEY = "Slf4jMDCFilter.ClientIP";

    private String responseHeader = DEFAULT_RESPONSE_TOKEN_HEADER;
    private String mdcTokenKey = DEFAULT_MDC_ID_TOKEN_KEY;
    private String mdcClientIpKey = DEFAULT_MDC_CLIENT_IP_KEY;
    private String requestHeader = DEFAULT_REQUEST_TOKEN_HEADER;

    @Bean
    public FilterRegistrationBean servletRegistrationBean() {
        final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        final Slf4jMDCFilter slf4jMDCFilter = new Slf4jMDCFilter(responseHeader, mdcTokenKey, mdcClientIpKey, requestHeader);
        registrationBean.setFilter(slf4jMDCFilter);
        registrationBean.setOrder(2);
        return registrationBean;
    }
}

4. Делаем фильтр. В нем извлекаем из REST-запроса HTTP заголовки "X-Token" (наш trace ID) и "X-Forvarded-For" (IP). Если заголовок "X-Token" отсутствует, то будет создан новый trace ID (генерируем рандомный UUID):

@Data
@EqualsAndHashCode(callSuper = false)
@EnableConfigurationProperties(Slf4jMDCFilterConfiguration.class)
@Component
public class Slf4jMDCFilter extends OncePerRequestFilter {

    private final String responseHeader;
    private final String mdcTokenKey;
    private final String mdcClientIpKey;
    private final String requestHeader;

    public Slf4jMDCFilter() {
        responseHeader = Slf4jMDCFilterConfiguration.DEFAULT_RESPONSE_TOKEN_HEADER;
        mdcTokenKey = Slf4jMDCFilterConfiguration.DEFAULT_MDC_ID_TOKEN_KEY;
        mdcClientIpKey = Slf4jMDCFilterConfiguration.DEFAULT_MDC_CLIENT_IP_KEY;
        requestHeader = Slf4jMDCFilterConfiguration.DEFAULT_REQUEST_TOKEN_HEADER;
    }

    public Slf4jMDCFilter(final String responseHeader, final String mdcTokenKey, final String mdcClientIPKey, final String requestHeader) {
        this.responseHeader = responseHeader;
        this.mdcTokenKey = mdcTokenKey;
        this.mdcClientIpKey = mdcClientIPKey;
        this.requestHeader = requestHeader;
    }

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
            throws java.io.IOException, ServletException {
        try {
            final String token = extractToken(request);
            final String clientIP = extractClientIP(request);
            MDC.put(mdcClientIpKey, clientIP);
            MDC.put(mdcTokenKey, token);
            if (!StringUtils.isEmpty(responseHeader)) {
                response.addHeader(responseHeader, token);
            }
            chain.doFilter(request, response);
        } finally {
            MDC.remove(mdcTokenKey);
            MDC.remove(mdcClientIpKey);
        }
    }

    private String extractToken(final HttpServletRequest request) {
        final String token;
        if (!StringUtils.isEmpty(requestHeader) && !StringUtils.isEmpty(request.getHeader(requestHeader))) {
            token = request.getHeader(requestHeader);
        } else {
            token = UUID.randomUUID().toString().toUpperCase().replace("-", "");
        }
        return token;
    }

    private String extractClientIP(final HttpServletRequest request) {
        final String clientIP;
        if (request.getHeader("X-Forwarded-For") != null) {
            clientIP = request.getHeader("X-Forwarded-For").split(",")[0];
        } else {
            clientIP = request.getRemoteAddr();
        }
        return clientIP;
    }

    @Override
    protected boolean isAsyncDispatch(final HttpServletRequest request) {
        return false;
    }

    @Override
    protected boolean shouldNotFilterErrorDispatch() {
        return false;
    }
}

5. Создаём "Hello!"-контроллер:

@Slf4j
@RestController
public class MyController {

    @GetMapping("/hello")
    public String getHello() {
        log.info("Called getHello");
        return "Hello!";
    }
}

6. Запускаем приложение и направляем ему два запроса:
1) Без заголовка "X-Token".

curl http://localhost:8080/hello

В ответ получаем "Hello!" и видим в логах запись с IP и сгенерированным токеном:

2019-01-19 16:13:52.965 INFO  [2B239745F1A4496CB5BF148FE9E1EDB9] [127.0.0.1] 31702 --- [nio-8080-exec-1] r.e.demo.mdc.controller.MyController     : Called getHello

2) С заголовком "X-Token".

curl -H "X-Token: 228dc00d216b" http://localhost:8080/hello

В ответ получаем "Hello!" и видим в логах запись с IP и переданным в заголовке токеном:

2019-01-19 16:16:33.889 INFO  [228dc00d216b] [127.0.0.1] 31702 --- [nio-8080-exec-3] r.e.demo.mdc.controller.MyController     : Called getHello


Код на GitHub

пятница, 18 января 2019 г.

Логирование в Spring Boot с глобальным traceId (применяем Logback и Spring Cloud Sleuth)

Предположим, нам необходимо по логам всех взаимодействующих друг с другом микросервисов отследить всю историю активности, которая привела к ошибке, начиная от REST-запроса с UI к микросервису A и заканчивая логированием ошибки "соседним" микросервисом B, к которому обратился сервис A.
С этой целью может быть использован так называемый TraceId - специальный глобальный ID, который автоматически будет добавлен логером в логируемое сообщение в любом месте, где мы пожелаем это сделать.

В моем проекте была поставлена следующая задача: сервис получает вместе с REST-запросом TraceId, который был присвоен потоку извне (например, nginx или другой сервис), и добавляет его в логируемые сообщения и в свои REST-запросы к другим сервисам.

Spring Cloud Sleuth предлагает необходимый функционал "из коробки". Что для этого нужно:

1\ Добавляем зависимость в pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
    <version>${spring-cloud-starter-sleuth.version}</version>
</dependency>

2\ Указываем имя нашего приложения в файле bootstrap.yml

spring:
  application:
    name: demo.sleuth

Зачем приложению имя? Оно будет добавлено в логируемое сообщение. Наличие имени - не обязательно. Можно обойтись и без него.
Почему указываем имя в bootstrap.yml? Потому что при использовании компонентов Spring Cloud предполагается, что конфигурация приложения находится именно в этом файле. Я пробовал указывать имя файла в application.properties и application.yml, но Spring Cloud Sleuth его там "не видел".

3\ Добавляем файл конфигурации logback-spring.xml

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <property name="LOG_FILE" value="log/demo.sleuth"/><property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <property name="FILE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" />

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${LOG_FILE}.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.log</FileNamePattern>
            <MaxHistory>7</MaxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>

4\ Запускаем проиложение и в терминале направляем ему GET-запрос на адрес http://localhost:8080/hello

curl http://localhost:8080/hello

В ответ получаем сообщение "Hello" и видим следующие логи:

2019-01-18 12:27:01.451  INFO [demo.sleuth,,,] 9442 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-01-18 12:27:01.752  INFO [demo.sleuth,,,] 9442 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-01-18 12:27:01.753  INFO [demo.sleuth,,,] 9442 --- [           main] r.e.demosleuth.DemoSleuthApplication     : Started DemoSleuthApplication in 2.132 seconds (JVM running for 2.497)
2019-01-18 12:27:12.215  INFO [demo.sleuth,,,] 9442 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-01-18 12:27:12.215  INFO [demo.sleuth,,,] 9442 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-01-18 12:27:12.224  INFO [demo.sleuth,,,] 9442 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 9 ms
2019-01-18 12:27:19.547  INFO [demo.sleuth,092c5c8569eb8bd6,092c5c8569eb8bd6,false] 9442 --- [nio-8080-exec-2] r.e.demosleuth.controller.MyController   : Called getHello

Записи в логе имеют вид [appname,traceId,spanId,exportable], где:
- appname - имя приложения;
- traceId - ID графа;
- spanId - ID отдельной операции в графе;
- exportable - ключ, который устанавливает, нужно ли экспортировать запись в Zipkin.
 TraceId и spanId - это 64-битные числа (тип long), представленные в шестнадцатеричном формате.

Spring Cloud Sleuth проверяет в поступившем REST-запросе наличие заголовков X-B3-TraceId и X-B3-SpanId и при их отсутствии генерирует значения traceId и spanId самостоятельно. В этом случае traceId и spanId будут одинакоывми, т.к. это будет первый span в этом trace.

5\ Направим приложению REST-запрос с заголовками X-B3-TraceId и X-B3-SpanId

curl -H "X-B3-TraceId: 6bfd228dc00d216b" -H "X-B3-SpanId: 46ab0d418373cbc9" http://localhost:8080/hello 

В ответ получим сообщение "Hello!" и  увидим следующий лог:

2019-01-18 13:10:33.972  INFO [demo.sleuth,6bfd228dc00d216b,46ab0d418373cbc9,false] 12297 --- [nio-8080-exec-3] r.e.demosleuth.controller.MyController   : Called getHello

В нём видим, что Sleuth распознал и применил заданные нами traceId и spanId.

Код на GitHub.

понедельник, 14 января 2019 г.

Логирование в Spring Boot с Logback

В интернете полно информации о разных фреймворках логирования. Но, в основном, она ограничивается настройкой и запуском простого "Hello World!".

Если нужно получить что-то большее, чем "Hello World!", бывает непросто разобраться в конфигурировании соответствующих логеров.

В силу "исторических" причин я выбрал Logback - он используется в Spring Boot в качестве логера по умолчанию, не нужно подключать дополнительные зависимости, разрешать конфликты с другими логерами и т.д. Есть Spring Boot - есть логер.

При разработке одного из проектов нужно было решить следующие задачи:
- логирование в консоль;
- логирование в файл;
- логирование в файл в формате JSON;
- логирование в системный журнал /var/log/syslog.
Logback справился со всеми этими задачами.

Что и как конфигурировал.
1) В файле pom.xml необходимо подключить дополнительные зависимости для логирования в формате JSON:

    <dependency>
      <groupId>ch.qos.logback.contrib</groupId>
      <artifactId>logback-json-classic</artifactId>
      <version>${logback.contrib.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback.contrib</groupId>
      <artifactId>logback-jackson</artifactId>
      <version>${logback.contrib.version}</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>${jackson.version}</version>
    </dependency>

2) В директории src/main/resources/ создать файл конфигурации logback-spring.xml. Согласно документации по Spring, рекомендуется задать именно такое имя конфигурационного файла. Если назвать файл logback.xml, как это советует документация по Logback, то могут возникнуть неочевидные проблемы с подхватыванием логером файла конфигурации.
В файле конфигурации указываем следующее:


<configuration>

<!--Аппендер для записи в файл-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>log/my_application.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>log/my_application.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--Сохранять файлы логов за последние 7 дней. Общий размер файлов не должен превышать 3GB-->
            <MaxHistory>7</MaxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

<!--Аппендер для записи в другой файл в формате JSON-->
    <appender name="JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>log/my_application.json</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>log/my_application.%d{yyyy-MM-dd}.json</FileNamePattern>
            <MaxHistory>7</MaxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
                <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>
                <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId>
                <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
                    <prettyPrint>true</prettyPrint>
                </jsonFormatter>
                <appendLineSeparator>true</appendLineSeparator>
            </layout>
        </encoder>
<!--Ниже можно переопределить уровень логирования по умолчанию-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level> 
        </filter>
    </appender>
    
<!--Аппендер для записи сообщений в системный журнал /var/log/syslog.
Чтобы сообщения начали писаться в системный журнал, нужно:
1\ раскомментировать следующие строки в файле /etc/rsyslog.conf:
        module(load="imudp")
        input(type="imudp" port="514")
2\ перезапустить сервис rsyslog:
        sudo service rsyslog restart
-->
    <appender name="UDP_APPENDER" class="ch.qos.logback.classic.net.SyslogAppender">
        <syslogHost>localhost</syslogHost>
        <facility>LOCAL0</facility>
<!--Здесь можно указать в суфиксе сообщения название своего приложения, чтобы отличить его записи в системном журнале от сообщений других приложений-->
        <suffixPattern>[my_application] [%thread] %-5level %logger{35} - %msg%n</suffixPattern>
    </appender>

<!--Аппендер для вывода сообщений в консоль-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
<!--Указываем логеру выводить в консоль раскрашенные сообщения-->
            <pattern>%cyan(%date{yyyy-MM-dd HH:mm:ss}) [%thread] %highlight(%-5level) %green(%logger{35}) - %msg%n</pattern>
        </encoder>
    </appender>

<!--Уровни логирования по умолчанию для аппендеров -->
    <root level="INFO">
        <appender-ref ref="FILE"/>
        <appender-ref ref="JSON"/>
        <appender-ref ref="UDP_APPENDER"/>
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>

После запуска приложения лог-файлы появятся в директории log/ в корне проекта.


среда, 2 января 2019 г.

Как скачать видео с Coursehunters

Погуглил на эту тему. Ничего простого не нашёл. Написал шелл-скрипт в одну строчку:


#!/bin/bash

wget `curl $1 | grep 'itemprop="contentUrl' | grep -Eo 'href="[^"]*.mp4' | cut -d\" -f2-`

Как пользоваться:
Скопировать скрипт в директорию /usr/bin/:

sudo cp ./chvd /usr/bin/

Сделать файл исполняемым:

sudo chmod +x /usr/bin/chvd

Перейти в директорию, в которую собираемся скачивать видео:

cd ~/Video/course-name

Запустить скрипт, передав ему в качестве параметра ссылку на html страницу с видео:

chvd https://coursehunters.net/course/sozdayte-12-faktornoe-prilozhenie-na-node-js-s-pomoshchyu-docker

Код на GitHub.