1 module syslog.service;
2 
3 import std.traits:isSomeString;
4 
5 import vibe.d;
6 
7 ///
8 static bool equalComponents(T,COMPONENTS...)(T _a, T _b)
9 {
10 	foreach(comp; COMPONENTS)
11 	{
12 		static assert(is(typeof(comp) : string), "components must be strings");
13 		
14 		if(__traits(getMember, _a, comp) != __traits(getMember, _b, comp))
15 			return false;
16 	}
17 	
18 	return true;
19 }
20 
21 unittest
22 {
23 	struct Test{
24 		int i;
25 		string s;
26 	}
27 
28 	Test t1 = Test(12,"foo");
29 	Test t2 = Test(12,"bar");
30 
31 	assert(equalComponents!(Test,"i")(t1,t2));
32 	assert(!equalComponents!(Test,"i","s")(t1,t2));
33 }
34 
35 ///
36 final class SysLogService
37 {
38 private:
39 	///
40 	public alias RequestCallback = void delegate(string, in string[], HTTPServerRequest);
41 
42 	bool m_quiet;
43 	ushort m_port = 8888;
44 	string m_hostName = "hostUnknown";
45 	string m_logFolder = "./";
46 	string m_fileSuffix = "";
47 	bool m_dashesInFileNameTimeStamp = false;
48 	bool m_oneLogPerHour = false;
49 	RequestCallback m_requestModifierCallback;
50 
51 	SysTime m_lastFileNameUpdateTime = SysTime.min;
52 	string m_lastFileName;
53 
54 	static immutable FORMATSTR_DAILY = "%04d-%02d-%02d_%s%s.log";
55 	static immutable FORMATSTR_DAILY_SHORT = "%04d%02d%02d%s%s.log";
56 	static immutable FORMATSTR_HOURLY = "%04d-%02d-%02d-%02d00_%s%s.log";
57 	static immutable FORMATSTR_HOURLY_SHORT = "%04d%02d%02d%02d00_%s%s.log";
58 
59 	///
60 	@property public void port(ushort _port) { m_port = _port; }
61 	///
62 	@property public void quiet(bool _quiet) { m_quiet = _quiet; }
63 	///
64 	@property public void hostName(string _hostname) { m_hostName = _hostname; }
65 	///
66 	@property public void fileSuffix(string _suffix) { m_fileSuffix = _suffix.length>0 ? "_"~_suffix : ""; }
67 	///
68 	@property public void oneLogPerHour(bool _value) { m_oneLogPerHour = _value; }
69 	///
70 	@property public void enableDashesInTimeStampOfLogFiles(bool _value) { m_dashesInFileNameTimeStamp = _value; }
71 	///
72 	@property public void logFolder(string _logFolder) { m_logFolder = _logFolder; }
73 	///
74 	@property public void requestModifierCallback(RequestCallback _cb) { m_requestModifierCallback = _cb; }
75 
76 	/++ 
77 	 + starts the http server.
78 	 + If not changed the default listens on port 8888, 
79 	 + is quiet (does not print every log msg to the stdout), 
80 	 + is named "hostUnknown" and 
81 	 + writes the logfiles to "./".
82 	 +/
83 	public void start()
84 	{
85 		auto settings = new HTTPServerSettings();
86 		settings.port = m_port;
87 		settings.options = HTTPServerOption.parseFormBody;
88 		settings.bindAddresses = ["0.0.0.0"];
89 		
90 		listenHTTP(settings, &handleRequest);
91 	}
92 
93 	///
94 	void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
95 	{
96 		res.statusCode = 200;
97 		res.headers["Content-Length"]="0";
98 		res.writeBody("");
99 
100 		if(req.requestURL.startsWith("/"))
101 			req.requestURL = req.requestURL[1..$];
102 
103 		auto eventNames = req.requestURL.split("/");
104 		
105 		if(eventNames.length == 0 || eventNames[$-1].length == 0)
106 		{
107 			logError("req has no event set: %s",req.requestURL);
108 			return;
109 		}
110 
111 		auto event = eventNames[$-1];
112 
113 		//note: ignore this default request
114 		if(event == "favicon.ico")
115 			return;
116 
117 		if(m_requestModifierCallback)
118 			m_requestModifierCallback(event, eventNames[0..$-1], req);
119 		
120 		syslog(eventNames, req.form, req.peer, req.clientAddress.port);
121 	}
122 
123 	///
124 	void syslog(string[] _events, FormFields _values, string _ip, ushort _port)
125 	{
126 		auto logline = createSyslogLine(_events, _values);
127 		
128 		if(!m_quiet)
129 			logInfo("%s",logline);
130 		
131 		appendToFile(m_logFolder ~ getLogFileName(), logline);
132 	}
133 
134 	///
135 	string getLogFileName()
136 	{
137 		auto currentTime = Clock.currTime();
138 
139 		if(!m_oneLogPerHour)
140 		{
141 			if(!equalComponents!(SysTime,"year","month","day")(currentTime,m_lastFileNameUpdateTime))
142 			{
143 				m_lastFileName = format(m_dashesInFileNameTimeStamp?FORMATSTR_DAILY:FORMATSTR_DAILY_SHORT,
144 		              currentTime.year,
145 		              currentTime.month,
146 		              currentTime.day,
147 		              m_hostName,
148 		              m_fileSuffix);
149 
150 				m_lastFileNameUpdateTime = currentTime;
151 			}
152 		}
153 		else
154 		{
155 			if(!equalComponents!(SysTime,"year","month","day","hour")(currentTime,m_lastFileNameUpdateTime))
156 			{
157 				m_lastFileName = format(m_dashesInFileNameTimeStamp?FORMATSTR_HOURLY:FORMATSTR_HOURLY_SHORT,
158 				                        currentTime.year,
159 				                        currentTime.month,
160 				                        currentTime.day,
161 				                        currentTime.hour,
162 				                        m_hostName,
163 				                        m_fileSuffix);
164 				
165 				m_lastFileNameUpdateTime = currentTime;
166 			}
167 		}
168 
169 		return m_lastFileName;
170 	}
171 
172 	///
173 	string getLogLineDate()
174 	{
175 		auto currentTime = Clock.currTime();
176 		
177 		enum monthNames = [ 
178 			"Jan", "Feb", "Mar", "Apr", "May", "Jun", 
179 			"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
180 		
181 		//Sep 11 00:02:14
182 		return format("%s %02d %02d:%02d:%02d",
183 		              monthNames[currentTime.month-1],
184 		currentTime.day,
185 		currentTime.hour,
186 		currentTime.minute,
187 		currentTime.second);
188 	}
189 
190 	///
191 	string createSyslogLine(string[] _events, FormFields _values)
192 	{
193 		import std.array;
194 		
195 		Appender!string line;
196 		
197 		line.put(getLogLineDate());
198 		
199 		line ~= " "; 
200 		line ~= m_hostName;
201 
202 		foreach(e; _events[0..$-1])
203 		{
204 			line ~= " ";
205 			line ~= e;
206 		}
207 
208 		line ~= " - ";
209 		line ~= _events[$-1];
210 		
211 		if(_values.length > 0)
212 		{
213 			line ~= " [";
214 			
215 			foreach(k, v; _values)
216 			{
217 				line ~= k;
218 				line ~= "=\"";
219 				line ~= v; 
220 				line ~= "\" ";
221 			}
222 			
223 			line ~= "]";
224 		}
225 		
226 		line ~= "\n";
227 		
228 		return line.data;
229 	}
230 }