2222import com .google .common .base .Preconditions ;
2323import com .google .common .collect .Sets ;
2424import org .apache .drill .common .exceptions .DrillRuntimeException ;
25+ import org .apache .drill .common .logical .FormatPluginConfig ;
26+ import org .apache .drill .common .logical .StoragePluginConfig ;
2527import org .apache .drill .exec .ExecConstants ;
28+ import org .apache .drill .exec .store .StoragePluginRegistry ;
29+ import org .apache .drill .exec .store .dfs .FileSystemConfig ;
30+ import org .apache .drill .exec .store .dfs .WorkspaceConfig ;
2631import org .apache .drill .exec .work .WorkManager ;
27- import org .glassfish .jersey .server .mvc .Viewable ;
2832import org .joda .time .DateTime ;
2933import org .joda .time .format .DateTimeFormat ;
3034import org .joda .time .format .DateTimeFormatter ;
3438import jakarta .annotation .security .RolesAllowed ;
3539import jakarta .inject .Inject ;
3640import jakarta .ws .rs .GET ;
41+ import jakarta .ws .rs .POST ;
3742import jakarta .ws .rs .Path ;
3843import jakarta .ws .rs .PathParam ;
3944import jakarta .ws .rs .Produces ;
4045import jakarta .ws .rs .core .HttpHeaders ;
4146import jakarta .ws .rs .core .MediaType ;
4247import jakarta .ws .rs .core .Response ;
43- import jakarta .ws .rs .core .SecurityContext ;
4448import jakarta .xml .bind .annotation .XmlRootElement ;
4549
4650import java .io .BufferedReader ;
6064public class LogsResources {
6165 private static final Logger logger = LoggerFactory .getLogger (LogsResources .class );
6266
63- @ Inject DrillRestServer .UserAuthEnabled authEnabled ;
64- @ Inject SecurityContext sc ;
67+ private static final String DFS_PLUGIN_NAME = "dfs" ;
68+ private static final String LOGS_WORKSPACE_NAME = "logs" ;
69+ private static final String DRILL_LOG_FORMAT_NAME = "drilllog" ;
70+
71+ // Regex to parse Drill's default logback format:
72+ // %date{ISO8601} [%thread] %-5level %logger{36} - %msg%n
73+ // Example: 2025-03-12T10:30:45,123 [main] INFO o.a.d.e.s.DrillbitContext - Starting
74+ private static final String DRILL_LOG_REGEX =
75+ "(\\ d{4}-\\ d{2}-\\ d{2}[T ]\\ d{2}:\\ d{2}:\\ d{2},\\ d+)\\ s+\\ [([^\\ ]]+)\\ ]\\ s+(\\ w+)\\ s+([^\\ s]+)\\ s+-\\ s+(.*)" ;
76+
6577 @ Inject WorkManager work ;
6678
6779 private static final FileFilter file_filter = new FileFilter () {
@@ -76,43 +88,55 @@ public boolean accept(File file) {
7688 @ GET
7789 @ Path ("/logs" )
7890 @ Produces (MediaType .TEXT_HTML )
79- public Viewable getLogs () {
80- Set <Log > logs = getLogsJSON ();
81- return ViewableWithPermissions .create (authEnabled .get (), "/rest/logs/list.ftl" , sc , logs );
91+ public Response getLogs () {
92+ return Response .seeOther (java .net .URI .create ("/sqllab#/logs" )).build ();
8293 }
8394
8495 @ GET
8596 @ Path ("/logs.json" )
8697 @ Produces (MediaType .APPLICATION_JSON )
87- public Set < Log > getLogsJSON () {
98+ public Response getLogsJSON () {
8899 Set <Log > logs = Sets .newTreeSet ();
89- File [] files = getLogFolder ().listFiles (file_filter );
90100
91- for (File file : files ) {
92- logs .add (new Log (file .getName (), file .length (), file .lastModified ()));
101+ String logDir = System .getenv ("DRILL_LOG_DIR" );
102+ if (logDir == null ) {
103+ return Response .ok (logs ).build ();
104+ }
105+
106+ File folder = new File (logDir );
107+ if (!folder .isDirectory ()) {
108+ logger .warn ("DRILL_LOG_DIR does not point to a valid directory: {}" , logDir );
109+ return Response .ok (logs ).build ();
110+ }
111+
112+ File [] files = folder .listFiles (file_filter );
113+ if (files != null ) {
114+ for (File file : files ) {
115+ logs .add (new Log (file .getName (), file .length (), file .lastModified ()));
116+ }
93117 }
94118
95- return logs ;
119+ return Response . ok ( logs ). build () ;
96120 }
97121
98122 @ GET
99123 @ Path ("/log/{name}/content" )
100124 @ Produces (MediaType .TEXT_HTML )
101- public Viewable getLog (@ PathParam ("name" ) String name ) throws IOException {
102- try {
103- LogContent content = getLogJSON (name );
104- return ViewableWithPermissions .create (authEnabled .get (), "/rest/logs/log.ftl" , sc , content );
105- } catch (Exception | Error e ) {
106- logger .error ("Exception was thrown when fetching log {} :\n {}" , name , e );
107- return ViewableWithPermissions .create (authEnabled .get (), "/rest/errorMessage.ftl" , sc , e );
108- }
125+ public Response getLog (@ PathParam ("name" ) String name ) {
126+ return Response .seeOther (java .net .URI .create ("/sqllab#/logs" )).build ();
109127 }
110128
111129 @ GET
112130 @ Path ("/log/{name}/content.json" )
113131 @ Produces (MediaType .APPLICATION_JSON )
114- public LogContent getLogJSON (@ PathParam ("name" ) final String name ) throws IOException {
115- File file = getFileByName (getLogFolder (), name );
132+ public Response getLogJSON (@ PathParam ("name" ) final String name ) throws IOException {
133+ File folder = getLogFolderSafe ();
134+ if (folder == null ) {
135+ return Response .status (Response .Status .SERVICE_UNAVAILABLE )
136+ .entity (new LogSetupResponse (false , "DRILL_LOG_DIR is not configured" ))
137+ .build ();
138+ }
139+ File file = getFileByName (folder , name );
116140
117141 final int maxLines = work .getContext ().getOptionManager ().getOption (ExecConstants .WEB_LOGS_MAX_LINES ).num_val .intValue ();
118142
@@ -131,27 +155,174 @@ protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
131155 cache .put (i ++, line );
132156 }
133157
134- return new LogContent (file .getName (), cache .values (), maxLines );
158+ return Response . ok ( new LogContent (file .getName (), cache .values (), maxLines )). build ( );
135159 }
136160 }
137161
138162 @ GET
139163 @ Path ("/log/{name}/download" )
140164 @ Produces (MediaType .TEXT_PLAIN )
141165 public Response getFullLog (@ PathParam ("name" ) final String name ) {
142- File file = getFileByName (getLogFolder (), name );
166+ File folder = getLogFolderSafe ();
167+ if (folder == null ) {
168+ return Response .status (Response .Status .SERVICE_UNAVAILABLE ).build ();
169+ }
170+ File file = getFileByName (folder , name );
143171 return Response .ok (file )
144172 .header (HttpHeaders .CONTENT_DISPOSITION , String .format ("attachment;filename=\" %s\" " , name ))
145173 .build ();
146174 }
147175
176+ @ GET
177+ @ Path ("/api/v1/logs/sql-status" )
178+ @ Produces (MediaType .APPLICATION_JSON )
179+ public Response getLogsSqlStatus () {
180+ Map <String , Object > status = new LinkedHashMap <>();
181+
182+ // Check if DRILL_LOG_DIR is set
183+ String logDir = System .getenv ("DRILL_LOG_DIR" );
184+ status .put ("logDirConfigured" , logDir != null );
185+ status .put ("logDir" , logDir );
186+
187+ // Check if the dfs.logs workspace exists
188+ boolean workspaceExists = false ;
189+ boolean formatExists = false ;
190+ try {
191+ StoragePluginRegistry storage = work .getContext ().getStorage ();
192+ StoragePluginConfig config = storage .getStoredConfig (DFS_PLUGIN_NAME );
193+ if (config instanceof FileSystemConfig ) {
194+ FileSystemConfig fsConfig = (FileSystemConfig ) config ;
195+ workspaceExists = fsConfig .getWorkspaces ().containsKey (LOGS_WORKSPACE_NAME );
196+ formatExists = fsConfig .getFormats ().containsKey (DRILL_LOG_FORMAT_NAME );
197+ }
198+ } catch (Exception e ) {
199+ logger .debug ("Error checking logs SQL status" , e );
200+ }
201+
202+ status .put ("workspaceExists" , workspaceExists );
203+ status .put ("formatExists" , formatExists );
204+ status .put ("ready" , workspaceExists && formatExists );
205+
206+ return Response .ok (status ).build ();
207+ }
208+
209+ @ POST
210+ @ Path ("/api/v1/logs/sql-setup" )
211+ @ Produces (MediaType .APPLICATION_JSON )
212+ public Response setupLogsSql () {
213+ String logDir = System .getenv ("DRILL_LOG_DIR" );
214+ if (logDir == null ) {
215+ return Response .status (Response .Status .BAD_REQUEST )
216+ .entity (new LogSetupResponse (false ,
217+ "DRILL_LOG_DIR environment variable is not set" ))
218+ .build ();
219+ }
220+
221+ try {
222+ StoragePluginRegistry storage = work .getContext ().getStorage ();
223+ StoragePluginConfig config = storage .getStoredConfig (DFS_PLUGIN_NAME );
224+
225+ if (!(config instanceof FileSystemConfig )) {
226+ return Response .status (Response .Status .INTERNAL_SERVER_ERROR )
227+ .entity (new LogSetupResponse (false ,
228+ "dfs plugin is not a file system plugin" ))
229+ .build ();
230+ }
231+
232+ FileSystemConfig fsConfig = (FileSystemConfig ) config ;
233+ boolean modified = false ;
234+
235+ // Add the logs workspace if it doesn't exist
236+ if (!fsConfig .getWorkspaces ().containsKey (LOGS_WORKSPACE_NAME )) {
237+ FileSystemConfig copy = fsConfig .copy ();
238+ copy .getWorkspaces ().put (LOGS_WORKSPACE_NAME ,
239+ new WorkspaceConfig (logDir , false , DRILL_LOG_FORMAT_NAME , false ));
240+ storage .put (DFS_PLUGIN_NAME , copy );
241+ modified = true ;
242+ logger .info ("Created dfs.logs workspace pointing to {}" , logDir );
243+ }
244+
245+ // Add the drilllog format if it doesn't exist
246+ StoragePluginConfig updatedConfig = storage .getStoredConfig (DFS_PLUGIN_NAME );
247+ if (updatedConfig instanceof FileSystemConfig ) {
248+ FileSystemConfig updatedFsConfig = (FileSystemConfig ) updatedConfig ;
249+ if (!updatedFsConfig .getFormats ().containsKey (DRILL_LOG_FORMAT_NAME )) {
250+ // Build the format config as JSON to avoid a compile-time dependency
251+ // on the contrib/format-log module
252+ String formatJson = "{"
253+ + "\" type\" : \" logRegex\" ,"
254+ + "\" regex\" : \" " + DRILL_LOG_REGEX .replace ("\\ " , "\\ \\ " ) + "\" ,"
255+ + "\" extension\" : \" log\" ,"
256+ + "\" maxErrors\" : 10,"
257+ + "\" schema\" : ["
258+ + " {\" fieldName\" : \" log_timestamp\" , \" fieldType\" : \" VARCHAR\" },"
259+ + " {\" fieldName\" : \" thread\" , \" fieldType\" : \" VARCHAR\" },"
260+ + " {\" fieldName\" : \" level\" , \" fieldType\" : \" VARCHAR\" },"
261+ + " {\" fieldName\" : \" logger\" , \" fieldType\" : \" VARCHAR\" },"
262+ + " {\" fieldName\" : \" message\" , \" fieldType\" : \" VARCHAR\" }"
263+ + "]}" ;
264+
265+ FormatPluginConfig logFormat = storage .mapper ()
266+ .readValue (formatJson , FormatPluginConfig .class );
267+ storage .putFormatPlugin (DFS_PLUGIN_NAME , DRILL_LOG_FORMAT_NAME , logFormat );
268+ modified = true ;
269+ logger .info ("Created drilllog format plugin for parsing Drill logs" );
270+ }
271+ }
272+
273+ String msg = modified
274+ ? "Log SQL workspace and format configured successfully"
275+ : "Log SQL workspace and format were already configured" ;
276+ return Response .ok (new LogSetupResponse (true , msg )).build ();
277+
278+ } catch (Exception e ) {
279+ logger .error ("Error setting up logs SQL workspace" , e );
280+ return Response .status (Response .Status .INTERNAL_SERVER_ERROR )
281+ .entity (new LogSetupResponse (false ,
282+ "Failed to configure: " + e .getMessage ()))
283+ .build ();
284+ }
285+ }
286+
287+ public static class LogSetupResponse {
288+ @ JsonProperty
289+ public final boolean success ;
290+ @ JsonProperty
291+ public final String message ;
292+
293+ public LogSetupResponse (boolean success , String message ) {
294+ this .success = success ;
295+ this .message = message ;
296+ }
297+ }
298+
148299 private File getLogFolder () {
149300 return new File (Preconditions .checkNotNull (System .getenv ("DRILL_LOG_DIR" ), "DRILL_LOG_DIR variable is not set" ));
150301 }
151302
303+ /**
304+ * Returns the log folder if DRILL_LOG_DIR is set and valid, or null otherwise.
305+ */
306+ private File getLogFolderSafe () {
307+ String logDir = System .getenv ("DRILL_LOG_DIR" );
308+ if (logDir == null ) {
309+ return null ;
310+ }
311+ File folder = new File (logDir );
312+ if (!folder .isDirectory ()) {
313+ return null ;
314+ }
315+ return folder ;
316+ }
317+
152318 private File getFileByName (File folder , final String name ) {
319+ // Prevent path traversal attacks
320+ if (name .contains (".." ) || name .contains ("/" ) || name .contains ("\\ " )) {
321+ throw new DrillRuntimeException ("Invalid log file name: " + name );
322+ }
323+
153324 File [] files = folder .listFiles ((dir , fileName ) -> fileName .equals (name ));
154- if (files .length == 0 ) {
325+ if (files == null || files .length == 0 ) {
155326 throw new DrillRuntimeException (name + " doesn't exist" );
156327 }
157328 return files [0 ];
0 commit comments