Add the AccessLogger plugin (#854)

This commit is contained in:
An Tao 2021-05-17 21:45:18 +08:00 committed by GitHub
parent 706fc70abc
commit 9a059aedef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 660 additions and 33 deletions

View File

@ -55,23 +55,23 @@ if (BUILD_DROGON_SHARED)
VERSION ${DROGON_VERSION}
SOVERSION ${DROGON_MAJOR_VERSION})
target_link_libraries(${PROJECT_NAME} PUBLIC Threads::Threads)
if(WIN32)
target_link_libraries(${PROJECT_NAME} PUBLIC Rpcrt4 ws2_32)
if(CMAKE_CXX_COMPILER_ID MATCHES MSVC)
# Ignore MSVC C4251 and C4275 warning of exporting std objects with no dll export
# We export class to facilitate maintenance, thus if you compile
# drogon on windows as a shared library, you will need to use
# exact same compiler for drogon and your app.
target_compile_options(${PROJECT_NAME} PUBLIC /wd4251 /wd4275)
endif()
endif()
if (WIN32)
target_link_libraries(${PROJECT_NAME} PUBLIC Rpcrt4 ws2_32)
if (CMAKE_CXX_COMPILER_ID MATCHES MSVC)
# Ignore MSVC C4251 and C4275 warning of exporting std objects with no dll export
# We export class to facilitate maintenance, thus if you compile
# drogon on windows as a shared library, you will need to use
# exact same compiler for drogon and your app.
target_compile_options(${PROJECT_NAME} PUBLIC /wd4251 /wd4275)
endif ()
endif ()
else (BUILD_DROGON_SHARED)
add_library(${PROJECT_NAME} STATIC)
endif (BUILD_DROGON_SHARED)
if (CMAKE_CXX_COMPILER_ID MATCHES GNU)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror)
endif()
endif ()
include(GenerateExportHeader)
generate_export_header(${PROJECT_NAME} EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/exports/drogon/exports.h)
@ -197,6 +197,7 @@ set(DROGON_SOURCES
lib/src/NotFound.cc
lib/src/PluginsManager.cc
lib/src/SecureSSLRedirector.cc
lib/src/AccessLogger.cc
lib/src/SessionManager.cc
lib/src/StaticFileRouter.cc
lib/src/TaskTimeoutFlag.cc
@ -281,10 +282,10 @@ endif (BUILD_REDIS)
if (NOT Hiredis_FOUND)
set(DROGON_SOURCES
${DROGON_SOURCES}
lib/src/RedisClientSkipped.cc
lib/src/RedisResultSkipped.cc
lib/src/RedisClientManagerSkipped.cc)
${DROGON_SOURCES}
lib/src/RedisClientSkipped.cc
lib/src/RedisResultSkipped.cc
lib/src/RedisClientManagerSkipped.cc)
endif (NOT Hiredis_FOUND)
if (BUILD_TESTING)
@ -334,24 +335,24 @@ if (COZ_PROFILING)
endif (COZ_PROFILING)
set(DROGON_SOURCES
${DROGON_SOURCES}
orm_lib/src/ArrayParser.cc
orm_lib/src/Criteria.cc
orm_lib/src/DbClient.cc
orm_lib/src/DbClientImpl.cc
orm_lib/src/DbClientLockFree.cc
orm_lib/src/DbConnection.cc
orm_lib/src/Exception.cc
orm_lib/src/Field.cc
orm_lib/src/Result.cc
orm_lib/src/Row.cc
orm_lib/src/SqlBinder.cc
orm_lib/src/TransactionImpl.cc
orm_lib/src/RestfulController.cc)
${DROGON_SOURCES}
orm_lib/src/ArrayParser.cc
orm_lib/src/Criteria.cc
orm_lib/src/DbClient.cc
orm_lib/src/DbClientImpl.cc
orm_lib/src/DbClientLockFree.cc
orm_lib/src/DbConnection.cc
orm_lib/src/Exception.cc
orm_lib/src/Field.cc
orm_lib/src/Result.cc
orm_lib/src/Row.cc
orm_lib/src/SqlBinder.cc
orm_lib/src/TransactionImpl.cc
orm_lib/src/RestfulController.cc)
if (pg_FOUND OR MySQL_FOUND OR SQLite3_FOUND)
set(DROGON_SOURCES
${DROGON_SOURCES}
orm_lib/src/DbClientManager.cc)
${DROGON_SOURCES}
orm_lib/src/DbClientManager.cc)
else (pg_FOUND OR MySQL_FOUND OR SQLite3_FOUND)
set(DROGON_SOURCES ${DROGON_SOURCES} lib/src/DbClientManagerSkipped.cc)
endif (pg_FOUND OR MySQL_FOUND OR SQLite3_FOUND)
@ -512,7 +513,8 @@ install(FILES ${DROGON_UTIL_HEADERS}
DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/utils)
set(DROGON_PLUGIN_HEADERS lib/inc/drogon/plugins/Plugin.h
lib/inc/drogon/plugins/SecureSSLRedirector.h)
lib/inc/drogon/plugins/SecureSSLRedirector.h
lib/inc/drogon/plugins/AccessLogger.h)
install(FILES ${DROGON_PLUGIN_HEADERS}
DESTINATION ${INSTALL_INCLUDE_DIR}/drogon/plugins)

View File

@ -28,6 +28,7 @@
#include <drogon/MultiPart.h>
#include <drogon/plugins/Plugin.h>
#include <drogon/plugins/SecureSSLRedirector.h>
#include <drogon/plugins/AccessLogger.h>
#include <drogon/Cookie.h>
#include <drogon/Session.h>
#include <drogon/IOThreadStorage.h>

View File

@ -0,0 +1,178 @@
/**
*
* AccessLogger.h
*
*/
#pragma once
#include <drogon/HttpRequest.h>
#include <drogon/HttpResponse.h>
#include <drogon/plugins/Plugin.h>
#include <trantor/utils/AsyncFileLogger.h>
#include <vector>
namespace drogon
{
namespace plugin
{
/**
* @brief This plugin is used to print all requests to the log.
*
* The json configuration is as follows:
*
* @code
{
"name": "drogon::plugin::AccessLogger",
"dependencies": [],
"config": {
"log_path": "./",
"log_format": "",
"log_file": "access.log",
"log_size_limit": 0,
"use_local_time": true,
"log_index": 0
}
}
@endcode
*
* log_format: a format string for access logging, there are several
* placeholders that represent particular data.
* $date: the time when the log was printed.
* $request_date: the time when the request was created.
* $request_path|$path: the path of the request.
* $request_query|$query: the query string of the request.
* $request_url|$url: the URL of the request, equals to
* $request_path+"?"+$request_query.
* $remote_addr: the remote address
* $local_addr: the local address
* $request_len|$body_bytes_received: the content length of the request.
* $method: the HTTP method of the request.
* $thread: the current thread number.
* $response_len|$body_bytes_sent: the content length of the response.
* $http_[header_name]: the header of the request.
* $cookie_[cookie_name]: the cookie of the request.
* $upstream_http_[header_name]: the header of the response sent to the
* client.
* $status_code: the status code of the response.
* $status: the status code and string of the response.
* $processing_time: request processing time in seconds with a microseconds
* resolution; time elapsed between the request object was
* created and response object was created.
* @note If the format string is empty or not configured, a default value of
* "$request_date $method $url [$body_bytes_received] ($remote_addr -
* $local_addr) $status $body_bytes_sent $processing_time" is applied.
*
* log_path: Log file path, empty by default,in which case,logs are output to
* the regular log file (or stdout based on the log configuration).
*
* log_file: The access log file name, 'access.log' by default. if the file name
* does not contain a extension, the .log extension is used.
*
* log_size_limit: 0 bytes by default, when the log file size reaches
* "log_size_limit", the log file is switched. Zero value means never switch
*
* log_index: The index of log output, 0 by default.
*
* Enable the plugin by adding the configuration to the list of plugins in the
* configuration file.
*
*/
class DROGON_EXPORT AccessLogger : public drogon::Plugin<AccessLogger>
{
public:
AccessLogger()
{
}
void initAndStart(const Json::Value &config) override;
void shutdown() override;
private:
trantor::AsyncFileLogger asyncFileLogger_;
int logIndex_{0};
bool useLocalTime_{true};
using LogFunction = std::function<void(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &)>;
std::vector<LogFunction> logFunctions_;
void logging(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp);
void createLogFunctions(std::string format);
LogFunction newLogFunction(const std::string &placeholder);
std::map<std::string, LogFunction> logFunctionMap_;
//$request_path
static void outputReqPath(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$request_query
static void outputReqQuery(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$request_url
static void outputReqURL(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$date
void outputDate(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) const;
//$request_date
void outputReqDate(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) const;
//$remote_addr
static void outputRemoteAddr(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$local_addr
static void outputLocalAddr(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$request_len $body_bytes_received
static void outputReqLength(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$response_len $body_bytes_sent
static void outputRespLength(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$method
static void outputMethod(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$thread
static void outputThreadNumber(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$http_[header_name]
static void outputReqHeader(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const std::string &headerName);
//$cookie_[cookie_name]
static void outputReqCookie(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const std::string &cookie);
//$upstream_http_[header_name]
static void outputRespHeader(trantor::LogStream &stream,
const drogon::HttpResponsePtr &resp,
const std::string &headerName);
//$status
static void outputStatusString(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$status_code
static void outputStatusCode(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$processing_time
static void outputProcessingTime(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
//$upstream_http_content-type $upstream_http_content_type
static void outputRespContentType(trantor::LogStream &,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &);
};
} // namespace plugin
} // namespace drogon

436
lib/src/AccessLogger.cc Normal file
View File

@ -0,0 +1,436 @@
/**
*
* @file AccessLogger.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "HttpUtils.h"
#include <drogon/drogon.h>
#include <drogon/plugins/AccessLogger.h>
#include <regex>
#include <thread>
#ifndef _WIN32
#include <unistd.h>
#include <sys/syscall.h>
#else
#include <sstream>
#endif
#ifdef __FreeBSD__
#include <pthread_np.h>
#endif
using namespace drogon;
using namespace drogon::plugin;
void AccessLogger::initAndStart(const Json::Value &config)
{
useLocalTime_ = config.get("use_local_time", true).asBool();
logFunctionMap_ = {{"$request_path", outputReqPath},
{"$path", outputReqPath},
{"$date",
[this](trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp) {
outputDate(stream, req, resp);
}},
{"$request_date",
[this](trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp) {
outputReqDate(stream, req, resp);
}},
{"$request_query", outputReqQuery},
{"$request_url", outputReqURL},
{"$query", outputReqQuery},
{"$url", outputReqURL},
{"$remote_addr", outputRemoteAddr},
{"$local_addr", outputLocalAddr},
{"$request_len", outputReqLength},
{"$body_bytes_received", outputReqLength},
{"$method", outputMethod},
{"$thread", outputThreadNumber},
{"$response_len", outputRespLength},
{"$body_bytes_sent", outputRespLength},
{"$status", outputStatusString},
{"$status_code", outputStatusCode},
{"$processing_time", outputProcessingTime},
{"$upstream_http_content-type", outputRespContentType},
{"$upstream_http_content_type", outputRespContentType}};
auto format = config.get("log_format", "").asString();
if (format.empty())
{
format =
"$request_date $method $url [$body_bytes_received] ($remote_addr - "
"$local_addr) $status $body_bytes_sent $processing_time";
}
createLogFunctions(format);
auto logPath = config.get("log_path", "").asString();
if (!logPath.empty())
{
auto fileName = config.get("log_file", "access.log").asString();
auto extension = std::string(".log");
auto pos = fileName.rfind('.');
if (pos != std::string::npos)
{
extension = fileName.substr(pos);
fileName = fileName.substr(0, pos);
}
if (fileName.empty())
{
fileName = "access";
}
asyncFileLogger_.setFileName(fileName, extension, logPath);
asyncFileLogger_.startLogging();
logIndex_ = config.get("log_index", 0).asInt();
trantor::Logger::setOutputFunction(
[&](const char *msg, const uint64_t len) {
asyncFileLogger_.output(msg, len);
},
[&]() { asyncFileLogger_.flush(); },
logIndex_);
auto sizeLimit = config.get("size_limit", 0).asUInt64();
if (sizeLimit > 0)
{
asyncFileLogger_.setFileSizeLimit(sizeLimit);
}
}
drogon::app().registerPreSendingAdvice(
[this](const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp) {
logging(LOG_RAW_TO(logIndex_), req, resp);
});
}
void AccessLogger::shutdown()
{
}
void AccessLogger::logging(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp)
{
for (auto &func : logFunctions_)
{
func(stream, req, resp);
}
}
void AccessLogger::createLogFunctions(std::string format)
{
std::string rawString;
while (!format.empty())
{
LOG_INFO << format;
auto pos = format.find('$');
if (pos != std::string::npos)
{
rawString += format.substr(0, pos);
format = format.substr(pos);
std::regex e{"^\\$[a-zA-Z0-9\\-_]+"};
std::smatch m;
if (std::regex_search(format, m, e))
{
if (!rawString.empty())
{
logFunctions_.emplace_back(
[rawString](trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) {
stream << rawString;
});
rawString.clear();
}
auto placeholder = m[0];
logFunctions_.emplace_back(newLogFunction(placeholder));
format = m.suffix().str();
}
else
{
rawString += '$';
format = format.substr(1);
}
}
else
{
rawString += format;
break;
}
}
if (!rawString.empty())
{
logFunctions_.emplace_back(
[rawString =
std::move(rawString)](trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) {
stream << rawString << "\n";
});
}
else
{
logFunctions_.emplace_back(
[](trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) { stream << "\n"; });
}
}
AccessLogger::LogFunction AccessLogger::newLogFunction(
const std::string &placeholder)
{
auto iter = logFunctionMap_.find(placeholder);
if (iter != logFunctionMap_.end())
{
return iter->second;
}
if (placeholder.find("$http_") == 0 && placeholder.size() > 6)
{
auto headerName = placeholder.substr(6);
return [headerName =
std::move(headerName)](trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &) {
outputReqHeader(stream, req, headerName);
};
}
if (placeholder.find("$cookie_") == 0 && placeholder.size() > 8)
{
auto cookieName = placeholder.substr(8);
return [cookieName =
std::move(cookieName)](trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &) {
outputReqCookie(stream, req, cookieName);
};
}
if (placeholder.find("$upstream_http_") == 0 && placeholder.size() > 15)
{
auto headerName = placeholder.substr(15);
return [headerName = std::move(
headerName)](trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &resp) {
outputRespHeader(stream, resp, headerName);
};
}
return [placeholder](trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) {
stream << placeholder;
};
}
void AccessLogger::outputReqPath(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
stream << req->path();
}
void AccessLogger::outputDate(trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &) const
{
if (useLocalTime_)
{
stream << trantor::Date::now().toFormattedStringLocal(true);
}
else
{
stream << trantor::Date::now().toFormattedString(true);
}
}
void AccessLogger::outputReqDate(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &) const
{
if (useLocalTime_)
{
stream << req->creationDate().toFormattedStringLocal(true);
}
else
{
stream << req->creationDate().toFormattedString(true);
}
}
//$request_query
void AccessLogger::outputReqQuery(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
stream << req->query();
}
//$request_url
void AccessLogger::outputReqURL(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
auto &query = req->query();
if (query.empty())
{
stream << req->path();
}
else
{
stream << req->path() << '?' << query;
}
}
void AccessLogger::outputRemoteAddr(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
stream << req->peerAddr().toIpPort();
}
void AccessLogger::outputLocalAddr(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
stream << req->localAddr().toIpPort();
}
void AccessLogger::outputReqLength(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
stream << req->body().length();
}
void AccessLogger::outputRespLength(trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &resp)
{
stream << resp->body().length();
}
void AccessLogger::outputMethod(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
stream << req->methodString();
}
void AccessLogger::outputThreadNumber(trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &)
{
#ifdef __linux__
static thread_local pid_t threadId_{0};
#else
static thread_local uint64_t threadId_{0};
#endif
#ifdef __linux__
if (threadId_ == 0)
threadId_ = static_cast<pid_t>(::syscall(SYS_gettid));
#elif defined __FreeBSD__
if (threadId_ == 0)
{
threadId_ = pthread_getthreadid_np();
}
#elif defined __OpenBSD__
if (threadId_ == 0)
{
threadId_ = getthrid();
}
#elif defined _WIN32
if (threadId_ == 0)
{
std::stringstream ss;
ss << std::this_thread::get_id();
threadId_ = std::stoull(ss.str());
}
#else
if (threadId_ == 0)
{
pthread_threadid_np(NULL, &threadId_);
}
#endif
stream << threadId_;
}
//$http_[header_name]
void AccessLogger::outputReqHeader(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const std::string &headerName)
{
stream << headerName << ": " << req->getHeader(headerName);
}
//$cookie_[cookie_name]
void AccessLogger::outputReqCookie(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const std::string &cookie)
{
stream << "(cookie)" << cookie << "=" << req->getCookie(cookie);
}
//$upstream_http_[header_name]
void AccessLogger::outputRespHeader(trantor::LogStream &stream,
const drogon::HttpResponsePtr &resp,
const std::string &headerName)
{
stream << headerName << ": " << resp->getHeader(headerName);
}
//$status
void AccessLogger::outputStatusString(trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &resp)
{
int code = resp->getStatusCode();
stream << code << " " << statusCodeToString(code);
}
//$status_code
void AccessLogger::outputStatusCode(trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &resp)
{
stream << resp->getStatusCode();
}
//$processing_time
void AccessLogger::outputProcessingTime(trantor::LogStream &stream,
const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &)
{
auto start = req->creationDate();
auto end = trantor::Date::now();
auto duration =
end.microSecondsSinceEpoch() - start.microSecondsSinceEpoch();
auto seconds = static_cast<double>(duration) / 1000000.0;
stream << seconds;
}
//$upstream_http_content-type $upstream_http_content_type
void AccessLogger::outputRespContentType(trantor::LogStream &stream,
const drogon::HttpRequestPtr &,
const drogon::HttpResponsePtr &resp)
{
auto typeStr = webContentTypeToString(resp->contentType());
if (typeStr.empty())
{
stream << "content-type: ";
}
else
{
if (typeStr.size() >= 2 && typeStr[typeStr.size() - 1] == '\n' &&
typeStr[typeStr.size() - 2] == '\r')
{
stream << string_view{typeStr.data(), typeStr.size() - 2};
}
else
{
stream << typeStr;
}
}
}

View File

@ -350,7 +350,7 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
for (auto it = cookies_.begin(); it != cookies_.end(); ++it)
{
output->append(it->first);
output->append("= ");
output->append("=");
output->append(it->second);
output->append(";");
}
@ -399,6 +399,11 @@ void HttpRequestImpl::addHeader(const char *start,
++cpos;
cookie_name = cookie_name.substr(cpos);
std::string cookie_value = coo.substr(epos + 1);
cpos = 0;
while (cpos < cookie_value.length() &&
isspace(cookie_value[cpos]))
++cpos;
cookie_value = cookie_value.substr(cpos);
cookies_[std::move(cookie_name)] = std::move(cookie_value);
}
value = value.substr(pos + 1);
@ -416,6 +421,11 @@ void HttpRequestImpl::addHeader(const char *start,
++cpos;
cookie_name = cookie_name.substr(cpos);
std::string cookie_value = coo.substr(epos + 1);
cpos = 0;
while (cpos < cookie_value.length() &&
isspace(cookie_value[cpos]))
++cpos;
cookie_value = cookie_value.substr(cpos);
cookies_[std::move(cookie_name)] = std::move(cookie_value);
}
}