/** * Copyright 2015 StreamSets Inc. * * Licensed under the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.streamsets.datacollector.restapi; import com.google.common.io.Resources; import com.streamsets.datacollector.log.LogStreamer; import com.streamsets.datacollector.log.LogUtils; import com.streamsets.datacollector.main.RuntimeInfo; import com.streamsets.datacollector.main.UserGroupManager; import com.streamsets.datacollector.restapi.bean.UserJson; import com.streamsets.datacollector.store.AclStoreTask; import com.streamsets.datacollector.store.PipelineInfo; import com.streamsets.datacollector.store.PipelineStoreTask; import com.streamsets.datacollector.store.impl.AclPipelineStoreTask; import com.streamsets.datacollector.util.AuthzRole; import com.streamsets.datacollector.util.PipelineException; import com.streamsets.lib.security.http.SSOPrincipal; import com.streamsets.pipeline.api.impl.Utils; import com.streamsets.pipeline.lib.parser.DataParserException; import com.streamsets.pipeline.lib.parser.shaded.org.aicer.grok.util.Grok; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.Authorization; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import javax.annotation.security.DenyAll; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Path("/v1/system") @Api(value = "system") @DenyAll public class LogResource { private static final String X_SDC_LOG_PREVIOUS_OFFSET_HEADER = "X-SDC-LOG-PREVIOUS-OFFSET"; private static final String EXCEPTION = "exception"; private static long MAX_EXCEPTION = 10 * 1024; // 10 KB private final RuntimeInfo runtimeInfo; private final PipelineStoreTask store; @Inject public LogResource( RuntimeInfo runtimeInfo, Principal principal, PipelineStoreTask store, AclStoreTask aclStore, UserGroupManager userGroupManager ) { this.runtimeInfo = runtimeInfo; UserJson currentUser; if (runtimeInfo.isDPMEnabled()) { currentUser = new UserJson((SSOPrincipal)principal); } else { currentUser = userGroupManager.getUser(principal); } if (runtimeInfo.isAclEnabled()) { this.store = new AclPipelineStoreTask(store, aclStore, currentUser); } else { this.store = store; } } @GET @Path("/logs") @ApiOperation(value= "Return latest log file contents") @Produces(MediaType.TEXT_PLAIN) @RolesAllowed({ AuthzRole.ADMIN, AuthzRole.CREATOR, AuthzRole.MANAGER, AuthzRole.ADMIN_REMOTE, AuthzRole.CREATOR_REMOTE, AuthzRole.MANAGER_REMOTE }) public Response currentLog( @QueryParam("endingOffset") @DefaultValue("-1") long startOffset, @QueryParam("extraMessage") String extraMessage, @QueryParam("pipeline") String pipeline, @QueryParam("severity") String severity, @Context SecurityContext context ) throws IOException, DataParserException, PipelineException { // Required for showing logs per pipeline in Pipeline page if (!context.isUserInRole(AuthzRole.ADMIN) && !context.isUserInRole(AuthzRole.ADMIN_REMOTE) && runtimeInfo.isAclEnabled() ) { Utils.checkNotNull(pipeline, "Pipeline name"); } if (!StringUtils.isEmpty(pipeline)) { // Validates Pipeline ACL Permission PipelineInfo pipelineInfo = store.getInfo(pipeline); pipeline = pipelineInfo.getTitle() + "/" + pipelineInfo.getPipelineId(); } String logFile = LogUtils.getLogFile(runtimeInfo); List<Map<String, String>> logData = new ArrayList<>(); long offset = startOffset; LogStreamer streamer = new LogStreamer(logFile, offset, 50 * 1024); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); streamer.stream(outputStream); if(extraMessage != null) { outputStream.write(extraMessage.getBytes(StandardCharsets.UTF_8)); } BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(outputStream.toByteArray()), StandardCharsets.UTF_8)); fetchLogData(bufferedReader, logData, pipeline, severity); offset = streamer.getNewEndingOffset(); streamer.close(); if((severity != null || pipeline != null) && logData.size() < 50) { //For filtering try to fetch more log data until it we get at least 50 lines of log data or it reaches top while (offset != 0 && logData.size() < 50) { streamer = new LogStreamer(logFile, offset, 50 * 1024); outputStream = new ByteArrayOutputStream(); streamer.stream(outputStream); //merge last message if it is part of new messages if (!logData.isEmpty() && logData.get(0).get("timestamp") == null && logData.get(0).get(EXCEPTION) != null) { outputStream.write(logData.get(0).get(EXCEPTION).getBytes(StandardCharsets.UTF_8)); logData.remove(0); } bufferedReader = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(outputStream.toByteArray()))); List<Map<String, String>> tempLogData = new ArrayList<>(); fetchLogData(bufferedReader, tempLogData, pipeline, severity); //Add newly fetched log data to the beginning of the list tempLogData.addAll(logData); logData = tempLogData; offset = streamer.getNewEndingOffset(); streamer.close(); } } return Response.ok() .type(MediaType.APPLICATION_JSON) .entity(logData) .header(X_SDC_LOG_PREVIOUS_OFFSET_HEADER, offset) .build(); } @GET @Path("/logs/files") @ApiOperation(value = "Returns all available SDC Log files", response = Map.class, responseContainer = "List", authorizations = @Authorization(value = "basic")) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({ AuthzRole.ADMIN, AuthzRole.ADMIN_REMOTE }) @SuppressWarnings("unchecked") public Response listLogFiles() throws IOException { File[] logFiles = LogUtils.getLogFiles(runtimeInfo); List<Map> list = new ArrayList<>(); for (File file : logFiles) { Map map = new HashMap(); map.put("file", file.getName()); map.put("lastModified", file.lastModified()); list.add(map); } return Response.ok(list).build(); } @GET @Path("/logs/files/{logName}") @ApiOperation(value = "Returns SDC Log File Content", response = String.class, authorizations = @Authorization(value = "basic")) @Produces(MediaType.TEXT_PLAIN) @RolesAllowed({ AuthzRole.ADMIN, AuthzRole.ADMIN_REMOTE }) public Response getLogFile( @PathParam("logName") String logName, @QueryParam("attachment") @DefaultValue("false") Boolean attachment ) throws IOException { Response response; File newLogFile = null; for (File file : LogUtils.getLogFiles(runtimeInfo)) { if (file.getName().equals(logName)) { newLogFile = file; break; } } if (newLogFile != null) { FileInputStream logStream = new FileInputStream(newLogFile); if(attachment) { return Response.ok(). header("Content-Disposition", "attachment; filename=" + logName).entity(logStream).build(); } else { response = Response.ok(logStream).build(); } } else { response = Response.status(Response.Status.NOT_FOUND).build(); } return response; } private void fetchLogData( BufferedReader bufferedReader, List<Map<String, String>> logData, String pipeline, String severity ) throws IOException, DataParserException { Grok logFileGrok = LogUtils.getLogGrok(runtimeInfo); String thisLine; boolean lastMessageFiltered = false; while ((thisLine = bufferedReader.readLine()) != null) { Map<String, String> namedGroupToValuesMap = logFileGrok.extractNamedGroups(thisLine); if(namedGroupToValuesMap != null) { if(severity != null && !severity.equals(namedGroupToValuesMap.get("severity"))) { lastMessageFiltered = true; continue; } if(pipeline != null && !pipeline.equals(namedGroupToValuesMap.get("s-entity"))) { lastMessageFiltered = true; continue; } lastMessageFiltered = false; logData.add(namedGroupToValuesMap); } else if(!lastMessageFiltered) { if(!logData.isEmpty()) { Map<String, String> lastLogData = logData.get(logData.size() - 1); if(lastLogData.containsKey(EXCEPTION)) { String exception = lastLogData.get(EXCEPTION); if(exception.length() <= MAX_EXCEPTION) { String newException; if(exception.length() + thisLine.length() > MAX_EXCEPTION) { newException = exception + "\n ... Truncated ..."; } else { newException = exception + "\n" + thisLine; } lastLogData.put(EXCEPTION, newException); } } else { lastLogData.put(EXCEPTION, thisLine); } } else { //First incomplete line Map<String, String> lastLogData = new HashMap<>(); lastLogData.put("exception", thisLine); logData.add(lastLogData); } } } } @GET @Path("/log/config") @Produces(MediaType.TEXT_PLAIN) @RolesAllowed({ AuthzRole.ADMIN, AuthzRole.ADMIN_REMOTE }) public Response getLogConfig( @QueryParam("default") @DefaultValue("false") boolean defaultConfig ) throws IOException { String fileName = runtimeInfo.getLog4jPropertiesFileName(); InputStream log4jProperties; if (defaultConfig) { log4jProperties = Resources.getResource(fileName + "-default").openStream(); } else { File file = new File(runtimeInfo.getConfigDir(), fileName); log4jProperties = new FileInputStream(file); } return Response.ok(log4jProperties).build(); } @POST @Path("/log/config") @Consumes(MediaType.TEXT_PLAIN) @RolesAllowed({ AuthzRole.ADMIN, AuthzRole.ADMIN_REMOTE }) public Response setLogConfig( InputStream payload ) throws IOException { File file = new File(runtimeInfo.getConfigDir(), runtimeInfo.getLog4jPropertiesFileName()); try (OutputStream os = new FileOutputStream(file)) { IOUtils.copy(payload, os); payload.close(); } return Response.ok().build(); } }