Skip to content

Commit 7823d8f

Browse files
committed
Introduce AccessLogger
1 parent a65f2d6 commit 7823d8f

13 files changed

+414
-9
lines changed

etc/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ install_if_not_exists(icinga2/conf.d/notifications.conf ${ICINGA2_CONFIGDIR}/con
3434
install_if_not_exists(icinga2/conf.d/templates.conf ${ICINGA2_CONFIGDIR}/conf.d)
3535
install_if_not_exists(icinga2/conf.d/timeperiods.conf ${ICINGA2_CONFIGDIR}/conf.d)
3636
install_if_not_exists(icinga2/conf.d/users.conf ${ICINGA2_CONFIGDIR}/conf.d)
37+
install_if_not_exists(icinga2/features-available/accesslog.conf ${ICINGA2_CONFIGDIR}/features-available)
3738
install_if_not_exists(icinga2/features-available/api.conf ${ICINGA2_CONFIGDIR}/features-available)
3839
install_if_not_exists(icinga2/features-available/debuglog.conf ${ICINGA2_CONFIGDIR}/features-available)
3940
install_if_not_exists(icinga2/features-available/mainlog.conf ${ICINGA2_CONFIGDIR}/features-available)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* The AccessLogger type writes API access information to a file.
3+
*/
4+
5+
object AccessLogger "access-log" {
6+
path = LogDir + "/access.log"
7+
}

lib/base/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
22

3+
mkclass_target(accesslogger.ti accesslogger-ti.cpp accesslogger-ti.hpp)
34
mkclass_target(application.ti application-ti.cpp application-ti.hpp)
45
mkclass_target(configobject.ti configobject-ti.cpp configobject-ti.hpp)
56
mkclass_target(configuration.ti configuration-ti.cpp configuration-ti.hpp)
@@ -14,6 +15,7 @@ mkclass_target(sysloglogger.ti sysloglogger-ti.cpp sysloglogger-ti.hpp)
1415

1516
set(base_SOURCES
1617
i2-base.hpp
18+
accesslogger.cpp accesslogger.hpp accesslogger-ti.hpp
1719
application.cpp application.hpp application-ti.hpp application-version.cpp application-environment.cpp
1820
array.cpp array.hpp array-script.cpp
1921
atomic.hpp

lib/base/accesslogger.cpp

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
2+
3+
#include "base/accesslogger.hpp"
4+
#include "base/accesslogger-ti.cpp"
5+
#include "base/configtype.hpp"
6+
#include "base/statsfunction.hpp"
7+
#include "base/application.hpp"
8+
#include <boost/algorithm/string.hpp>
9+
#include <boost/regex.hpp>
10+
#include <cstring>
11+
#include <unordered_map>
12+
#include <stdexcept>
13+
#include <time.h>
14+
#include <utility>
15+
#include <vector>
16+
17+
using namespace icinga;
18+
namespace http = boost::beast::http;
19+
20+
static std::set<AccessLogger::Ptr> l_AccessLoggers;
21+
static boost::mutex l_AccessLoggersMutex;
22+
23+
REGISTER_TYPE(AccessLogger);
24+
25+
REGISTER_STATSFUNCTION(AccessLogger, &AccessLogger::StatsFunc);
26+
27+
void AccessLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
28+
{
29+
DictionaryData nodes;
30+
31+
for (const AccessLogger::Ptr& accesslogger : ConfigType::GetObjectsByType<AccessLogger>()) {
32+
nodes.emplace_back(accesslogger->GetName(), 1); //add more stats
33+
}
34+
35+
status->Set("accesslogger", new Dictionary(std::move(nodes)));
36+
}
37+
38+
LogAccess::~LogAccess()
39+
{
40+
decltype(l_AccessLoggers) loggers;
41+
42+
{
43+
boost::mutex::scoped_lock lock (l_AccessLoggersMutex);
44+
loggers = l_AccessLoggers;
45+
}
46+
47+
for (auto& logger : loggers) {
48+
ObjectLock oLock (logger);
49+
logger->m_Formatter(*this, *logger->m_Stream);
50+
}
51+
}
52+
53+
void AccessLogger::Register()
54+
{
55+
boost::mutex::scoped_lock lock (l_AccessLoggersMutex);
56+
l_AccessLoggers.insert(this);
57+
}
58+
59+
void AccessLogger::Unregister()
60+
{
61+
boost::mutex::scoped_lock lock (l_AccessLoggersMutex);
62+
l_AccessLoggers.erase(this);
63+
}
64+
65+
void AccessLogger::OnAllConfigLoaded()
66+
{
67+
ObjectImpl<AccessLogger>::OnAllConfigLoaded();
68+
69+
m_Formatter = ParseFormatter(GetFormat());
70+
}
71+
72+
void AccessLogger::ValidateFormat(const Lazy<String>& lvalue, const ValidationUtils& utils)
73+
{
74+
ObjectImpl<AccessLogger>::ValidateFormat(lvalue, utils);
75+
76+
try {
77+
ParseFormatter(lvalue());
78+
} catch (const std::invalid_argument& ex) {
79+
BOOST_THROW_EXCEPTION(ValidationError(this, { "format" }, ex.what()));
80+
}
81+
}
82+
83+
template<class Message>
84+
static inline
85+
void StreamHttpProtocol(const Message& in, std::ostream& out)
86+
{
87+
auto protocol (in.version());
88+
auto minor (protocol % 10u);
89+
90+
out << (protocol / 10u);
91+
92+
if (minor || protocol != 20u) {
93+
out << '.' << minor;
94+
}
95+
}
96+
97+
template<class Message>
98+
static inline
99+
void StreamHttpCLength(const Message& in, std::ostream& out)
100+
{
101+
if (in.count(http::field::content_length)) {
102+
out << in[http::field::content_length];
103+
} else {
104+
out << '-';
105+
}
106+
}
107+
108+
template<class Message, class Header>
109+
static inline
110+
void StreamHttpHeader(const Message& in, const Header& header, std::ostream& out)
111+
{
112+
if (in.count(header)) {
113+
out << in[header];
114+
} else {
115+
out << '-';
116+
}
117+
}
118+
119+
static boost::regex l_ALFTime (R"EOF(\Atime.(.*)\z)EOF", boost::regex::mod_s);
120+
static boost::regex l_ALFHeaders (R"EOF(\A(request|response).headers.(.*)\z)EOF", boost::regex::mod_s);
121+
122+
static std::unordered_map<std::string, AccessLogger::FormatterFunc*> l_ALFormatters ({
123+
{ "local.address", [](const LogAccess& in, std::ostream& out) {
124+
out << in.Stream.lowest_layer().local_endpoint().address();
125+
} },
126+
{ "local.port", [](const LogAccess& in, std::ostream& out) {
127+
out << in.Stream.lowest_layer().local_endpoint().port();
128+
} },
129+
{ "remote.address", [](const LogAccess& in, std::ostream& out) {
130+
out << in.Stream.lowest_layer().remote_endpoint().address();
131+
} },
132+
{ "remote.port", [](const LogAccess& in, std::ostream& out) {
133+
out << in.Stream.lowest_layer().remote_endpoint().port();
134+
} },
135+
{ "remote.user", [](const LogAccess& in, std::ostream& out) {
136+
if (in.User.IsEmpty()) {
137+
out << '-';
138+
} else {
139+
out << in.User;
140+
}
141+
} },
142+
{ "request.method", [](const LogAccess& in, std::ostream& out) {
143+
out << in.Request.method();
144+
} },
145+
{ "request.uri", [](const LogAccess& in, std::ostream& out) {
146+
out << in.Request.target();
147+
} },
148+
{ "request.protocol", [](const LogAccess& in, std::ostream& out) {
149+
StreamHttpProtocol(in.Request, out);
150+
} },
151+
{ "request.size", [](const LogAccess& in, std::ostream& out) {
152+
StreamHttpCLength(in.Request, out);
153+
} },
154+
{ "response.protocol", [](const LogAccess& in, std::ostream& out) {
155+
StreamHttpProtocol(in.Response, out);
156+
} },
157+
{ "response.status", [](const LogAccess& in, std::ostream& out) {
158+
out << (int)in.Response.result();
159+
} },
160+
{ "response.reason", [](const LogAccess& in, std::ostream& out) {
161+
out << in.Response.reason();
162+
} },
163+
{ "response.size", [](const LogAccess& in, std::ostream& out) {
164+
StreamHttpCLength(in.Response, out);
165+
} }
166+
});
167+
168+
AccessLogger::Formatter AccessLogger::ParseFormatter(const String& format)
169+
{
170+
std::vector<std::string> tokens;
171+
boost::algorithm::split(tokens, format.GetData(), boost::algorithm::is_any_of("$"));
172+
173+
if (tokens.size() % 2u == 0u) {
174+
throw std::invalid_argument("Closing $ not found in macro format string '" + format + "'.");
175+
}
176+
177+
std::vector<Formatter> formatters;
178+
std::string literal;
179+
bool isLiteral = true;
180+
181+
for (auto& token : tokens) {
182+
if (isLiteral) {
183+
literal += token;
184+
} else if (token.empty()) {
185+
literal += "$";
186+
} else {
187+
if (!literal.empty()) {
188+
formatters.emplace_back([literal](const LogAccess&, std::ostream& out) {
189+
out << literal;
190+
});
191+
192+
literal = "";
193+
}
194+
195+
auto formatter (l_ALFormatters.find(token));
196+
197+
if (formatter == l_ALFormatters.end()) {
198+
boost::smatch what;
199+
200+
if (boost::regex_search(token, what, l_ALFTime)) {
201+
auto spec (what[1].str());
202+
203+
formatters.emplace_back([spec](const LogAccess&, std::ostream& out) {
204+
time_t now;
205+
struct tm tmNow;
206+
207+
(void)time(&now);
208+
(void)localtime_r(&now, &tmNow);
209+
210+
for (std::vector<char>::size_type size = 64;; size *= 2u) {
211+
std::vector<char> buf (size);
212+
213+
if (strftime(buf.data(), size, spec.data(), &tmNow)) {
214+
out << buf.data();
215+
break;
216+
} else if (!strlen(spec.data())) {
217+
break;
218+
}
219+
}
220+
});
221+
} else if (boost::regex_search(token, what, l_ALFHeaders)) {
222+
auto header (what[2].str());
223+
224+
if (what[1] == "request") {
225+
formatters.emplace_back([header](const LogAccess& in, std::ostream& out) {
226+
StreamHttpHeader(in.Request, header, out);
227+
});
228+
} else {
229+
formatters.emplace_back([header](const LogAccess& in, std::ostream& out) {
230+
StreamHttpHeader(in.Response, header, out);
231+
});
232+
}
233+
} else {
234+
throw std::invalid_argument("Bad macro '" + token + "'.");
235+
}
236+
} else {
237+
formatters.emplace_back(formatter->second);
238+
}
239+
}
240+
241+
isLiteral = !isLiteral;
242+
}
243+
244+
if (!literal.empty()) {
245+
formatters.emplace_back([literal](const LogAccess&, std::ostream& out) {
246+
out << literal;
247+
});
248+
}
249+
250+
switch (formatters.size()) {
251+
case 0u:
252+
return [](const LogAccess&, std::ostream&) { };
253+
case 1u:
254+
return std::move(formatters[0]);
255+
default:
256+
return [formatters](const LogAccess& in, std::ostream& out) {
257+
for (auto& formatter : formatters) {
258+
formatter(in, out);
259+
}
260+
};
261+
}
262+
}

lib/base/accesslogger.hpp

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
2+
3+
#ifndef ACCESSLOGGER_H
4+
#define ACCESSLOGGER_H
5+
6+
#include "base/i2-base.hpp"
7+
#include "base/accesslogger-ti.hpp"
8+
#include "base/shared.hpp"
9+
#include "base/tlsstream.hpp"
10+
#include <boost/beast/core.hpp>
11+
#include <boost/beast/http.hpp>
12+
#include <functional>
13+
#include <utility>
14+
15+
namespace icinga
16+
{
17+
18+
class LogAccess
19+
{
20+
public:
21+
inline LogAccess(
22+
const AsioTlsStream& stream,
23+
const boost::beast::http::request<boost::beast::http::string_body>& request,
24+
String user,
25+
const boost::beast::http::response<boost::beast::http::string_body>& response
26+
) : Stream(stream), Request(request), User(std::move(user)), Response(response)
27+
{}
28+
29+
LogAccess(const LogAccess&) = delete;
30+
LogAccess(LogAccess&&) = delete;
31+
LogAccess& operator=(const LogAccess&) = delete;
32+
LogAccess& operator=(LogAccess&&) = delete;
33+
~LogAccess();
34+
35+
const AsioTlsStream& Stream;
36+
const boost::beast::http::request<boost::beast::http::string_body>& Request;
37+
String User;
38+
const boost::beast::http::response<boost::beast::http::string_body>& Response;
39+
};
40+
41+
/**
42+
* A file logger that logs API access.
43+
*
44+
* @ingroup base
45+
*/
46+
class AccessLogger final : public ObjectImpl<AccessLogger>
47+
{
48+
friend LogAccess;
49+
50+
public:
51+
typedef void FormatterFunc(const LogAccess& in, std::ostream& out);
52+
typedef std::function<FormatterFunc> Formatter;
53+
54+
DECLARE_OBJECT(AccessLogger);
55+
DECLARE_OBJECTNAME(AccessLogger);
56+
57+
static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
58+
59+
protected:
60+
void OnAllConfigLoaded() override;
61+
void ValidateFormat(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
62+
void Register() override;
63+
void Unregister() override;
64+
65+
private:
66+
Formatter ParseFormatter(const String& format);
67+
68+
Formatter m_Formatter;
69+
};
70+
71+
}
72+
73+
#endif /* ACCESSLOGGER_H */

lib/base/accesslogger.ti

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
2+
3+
#include "base/filelogger.hpp"
4+
5+
library base;
6+
7+
namespace icinga
8+
{
9+
10+
class AccessLogger : FileLogger
11+
{
12+
activation_priority -99;
13+
14+
[config] String format {
15+
default {{{ return R"EOF($remote.address$ - $remote.user$ [$time.%d/%b/%Y:%T %z$] "$request.method$ $request.uri$ HTTP/$request.protocol$" $response.status$ $response.size$
16+
)EOF"; }}}
17+
};
18+
};
19+
20+
}

lib/base/filelogger.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ void FileLogger::Start(bool runtimeCreated)
3535

3636
ObjectImpl<FileLogger>::Start(runtimeCreated);
3737

38-
Log(LogInformation, "FileLogger")
38+
Log(LogInformation, GetReflectionType()->GetName())
3939
<< "'" << GetName() << "' started.";
4040
}
4141

0 commit comments

Comments
 (0)