001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.management.mbean; 018 019import java.io.ByteArrayInputStream; 020import java.io.InputStream; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.concurrent.TimeUnit; 029import java.util.concurrent.atomic.AtomicBoolean; 030import javax.management.AttributeValueExp; 031import javax.management.MBeanServer; 032import javax.management.ObjectName; 033import javax.management.Query; 034import javax.management.QueryExp; 035import javax.management.StringValueExp; 036 037import org.w3c.dom.Document; 038 039import org.apache.camel.CamelContext; 040import org.apache.camel.ManagementStatisticsLevel; 041import org.apache.camel.Route; 042import org.apache.camel.ServiceStatus; 043import org.apache.camel.TimerListener; 044import org.apache.camel.api.management.ManagedResource; 045import org.apache.camel.api.management.mbean.ManagedProcessorMBean; 046import org.apache.camel.api.management.mbean.ManagedRouteMBean; 047import org.apache.camel.model.ModelCamelContext; 048import org.apache.camel.model.ModelHelper; 049import org.apache.camel.model.RouteDefinition; 050import org.apache.camel.spi.InflightRepository; 051import org.apache.camel.spi.ManagementStrategy; 052import org.apache.camel.spi.RouteError; 053import org.apache.camel.spi.RoutePolicy; 054import org.apache.camel.util.ObjectHelper; 055import org.apache.camel.util.XmlLineNumberParser; 056import org.slf4j.Logger; 057import org.slf4j.LoggerFactory; 058 059@ManagedResource(description = "Managed Route") 060public class ManagedRoute extends ManagedPerformanceCounter implements TimerListener, ManagedRouteMBean { 061 062 public static final String VALUE_UNKNOWN = "Unknown"; 063 064 private static final Logger LOG = LoggerFactory.getLogger(ManagedRoute.class); 065 066 protected final Route route; 067 protected final String description; 068 protected final ModelCamelContext context; 069 private final LoadTriplet load = new LoadTriplet(); 070 private final String jmxDomain; 071 072 public ManagedRoute(ModelCamelContext context, Route route) { 073 this.route = route; 074 this.context = context; 075 this.description = route.getDescription(); 076 this.jmxDomain = context.getManagementStrategy().getManagementAgent().getMBeanObjectDomainName(); 077 } 078 079 @Override 080 public void init(ManagementStrategy strategy) { 081 super.init(strategy); 082 boolean enabled = context.getManagementStrategy().getManagementAgent().getStatisticsLevel() != ManagementStatisticsLevel.Off; 083 setStatisticsEnabled(enabled); 084 } 085 086 public Route getRoute() { 087 return route; 088 } 089 090 public CamelContext getContext() { 091 return context; 092 } 093 094 public String getRouteId() { 095 String id = route.getId(); 096 if (id == null) { 097 id = VALUE_UNKNOWN; 098 } 099 return id; 100 } 101 102 public String getDescription() { 103 return description; 104 } 105 106 @Override 107 public String getEndpointUri() { 108 if (route.getEndpoint() != null) { 109 return route.getEndpoint().getEndpointUri(); 110 } 111 return VALUE_UNKNOWN; 112 } 113 114 public String getState() { 115 // must use String type to be sure remote JMX can read the attribute without requiring Camel classes. 116 ServiceStatus status = context.getRouteStatus(route.getId()); 117 // if no status exists then its stopped 118 if (status == null) { 119 status = ServiceStatus.Stopped; 120 } 121 return status.name(); 122 } 123 124 public String getUptime() { 125 return route.getUptime(); 126 } 127 128 public long getUptimeMillis() { 129 return route.getUptimeMillis(); 130 } 131 132 public Integer getInflightExchanges() { 133 return (int) super.getExchangesInflight(); 134 } 135 136 public String getCamelId() { 137 return context.getName(); 138 } 139 140 public String getCamelManagementName() { 141 return context.getManagementName(); 142 } 143 144 public Boolean getTracing() { 145 return route.getRouteContext().isTracing(); 146 } 147 148 public void setTracing(Boolean tracing) { 149 route.getRouteContext().setTracing(tracing); 150 } 151 152 public Boolean getMessageHistory() { 153 return route.getRouteContext().isMessageHistory(); 154 } 155 156 public Boolean getLogMask() { 157 return route.getRouteContext().isLogMask(); 158 } 159 160 public String getRoutePolicyList() { 161 List<RoutePolicy> policyList = route.getRouteContext().getRoutePolicyList(); 162 163 if (policyList == null || policyList.isEmpty()) { 164 // return an empty string to have it displayed nicely in JMX consoles 165 return ""; 166 } 167 168 StringBuilder sb = new StringBuilder(); 169 for (int i = 0; i < policyList.size(); i++) { 170 RoutePolicy policy = policyList.get(i); 171 sb.append(policy.getClass().getSimpleName()); 172 sb.append("(").append(ObjectHelper.getIdentityHashCode(policy)).append(")"); 173 if (i < policyList.size() - 1) { 174 sb.append(", "); 175 } 176 } 177 return sb.toString(); 178 } 179 180 public String getLoad01() { 181 double load1 = load.getLoad1(); 182 if (Double.isNaN(load1)) { 183 // empty string if load statistics is disabled 184 return ""; 185 } else { 186 return String.format("%.2f", load1); 187 } 188 } 189 190 public String getLoad05() { 191 double load5 = load.getLoad5(); 192 if (Double.isNaN(load5)) { 193 // empty string if load statistics is disabled 194 return ""; 195 } else { 196 return String.format("%.2f", load5); 197 } 198 } 199 200 public String getLoad15() { 201 double load15 = load.getLoad15(); 202 if (Double.isNaN(load15)) { 203 // empty string if load statistics is disabled 204 return ""; 205 } else { 206 return String.format("%.2f", load15); 207 } 208 } 209 210 @Override 211 public void onTimer() { 212 load.update(getInflightExchanges()); 213 } 214 215 public void start() throws Exception { 216 if (!context.getStatus().isStarted()) { 217 throw new IllegalArgumentException("CamelContext is not started"); 218 } 219 context.getRouteController().startRoute(getRouteId()); 220 } 221 222 public void stop() throws Exception { 223 if (!context.getStatus().isStarted()) { 224 throw new IllegalArgumentException("CamelContext is not started"); 225 } 226 context.getRouteController().stopRoute(getRouteId()); 227 } 228 229 public void stop(long timeout) throws Exception { 230 if (!context.getStatus().isStarted()) { 231 throw new IllegalArgumentException("CamelContext is not started"); 232 } 233 context.getRouteController().stopRoute(getRouteId(), timeout, TimeUnit.SECONDS); 234 } 235 236 public boolean stop(Long timeout, Boolean abortAfterTimeout) throws Exception { 237 if (!context.getStatus().isStarted()) { 238 throw new IllegalArgumentException("CamelContext is not started"); 239 } 240 return context.getRouteController().stopRoute(getRouteId(), timeout, TimeUnit.SECONDS, abortAfterTimeout); 241 } 242 243 public void shutdown() throws Exception { 244 if (!context.getStatus().isStarted()) { 245 throw new IllegalArgumentException("CamelContext is not started"); 246 } 247 String routeId = getRouteId(); 248 context.stopRoute(routeId); 249 context.removeRoute(routeId); 250 } 251 252 public void shutdown(long timeout) throws Exception { 253 if (!context.getStatus().isStarted()) { 254 throw new IllegalArgumentException("CamelContext is not started"); 255 } 256 String routeId = getRouteId(); 257 context.stopRoute(routeId, timeout, TimeUnit.SECONDS); 258 context.removeRoute(routeId); 259 } 260 261 public boolean remove() throws Exception { 262 if (!context.getStatus().isStarted()) { 263 throw new IllegalArgumentException("CamelContext is not started"); 264 } 265 return context.removeRoute(getRouteId()); 266 } 267 268 @Override 269 public void restart() throws Exception { 270 restart(1); 271 } 272 273 @Override 274 public void restart(long delay) throws Exception { 275 stop(); 276 if (delay > 0) { 277 try { 278 LOG.debug("Sleeping {} seconds before starting route: {}", delay, getRouteId()); 279 Thread.sleep(delay * 1000); 280 } catch (InterruptedException e) { 281 // ignore 282 } 283 } 284 start(); 285 } 286 287 public String dumpRouteAsXml() throws Exception { 288 return dumpRouteAsXml(false); 289 } 290 291 @Override 292 public String dumpRouteAsXml(boolean resolvePlaceholders) throws Exception { 293 String id = route.getId(); 294 RouteDefinition def = context.getRouteDefinition(id); 295 if (def != null) { 296 String xml = ModelHelper.dumpModelAsXml(context, def); 297 298 // if resolving placeholders we parse the xml, and resolve the property placeholders during parsing 299 if (resolvePlaceholders) { 300 final AtomicBoolean changed = new AtomicBoolean(); 301 InputStream is = new ByteArrayInputStream(xml.getBytes("UTF-8")); 302 Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() { 303 @Override 304 public String transform(String text) { 305 try { 306 String after = getContext().resolvePropertyPlaceholders(text); 307 if (!changed.get()) { 308 changed.set(!text.equals(after)); 309 } 310 return after; 311 } catch (Exception e) { 312 // ignore 313 return text; 314 } 315 } 316 }); 317 // okay there were some property placeholder replaced so re-create the model 318 if (changed.get()) { 319 xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom); 320 RouteDefinition copy = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class); 321 xml = ModelHelper.dumpModelAsXml(context, copy); 322 } 323 } 324 return xml; 325 } 326 return null; 327 } 328 329 public void updateRouteFromXml(String xml) throws Exception { 330 // convert to model from xml 331 RouteDefinition def = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class); 332 if (def == null) { 333 return; 334 } 335 336 // if the xml does not contain the route-id then we fix this by adding the actual route id 337 // this may be needed if the route-id was auto-generated, as the intend is to update this route 338 // and not add a new route, adding a new route, use the MBean operation on ManagedCamelContext instead. 339 if (ObjectHelper.isEmpty(def.getId())) { 340 def.setId(getRouteId()); 341 } else if (!def.getId().equals(getRouteId())) { 342 throw new IllegalArgumentException("Cannot update route from XML as routeIds does not match. routeId: " 343 + getRouteId() + ", routeId from XML: " + def.getId()); 344 } 345 346 LOG.debug("Updating route: {} from xml: {}", def.getId(), xml); 347 348 try { 349 // add will remove existing route first 350 context.addRouteDefinition(def); 351 } catch (Exception e) { 352 // log the error as warn as the management api may be invoked remotely over JMX which does not propagate such exception 353 String msg = "Error updating route: " + def.getId() + " from xml: " + xml + " due: " + e.getMessage(); 354 LOG.warn(msg, e); 355 throw e; 356 } 357 } 358 359 public String dumpRouteStatsAsXml(boolean fullStats, boolean includeProcessors) throws Exception { 360 // in this logic we need to calculate the accumulated processing time for the processor in the route 361 // and hence why the logic is a bit more complicated to do this, as we need to calculate that from 362 // the bottom -> top of the route but this information is valuable for profiling routes 363 StringBuilder sb = new StringBuilder(); 364 365 // need to calculate this value first, as we need that value for the route stat 366 Long processorAccumulatedTime = 0L; 367 368 // gather all the processors for this route, which requires JMX 369 if (includeProcessors) { 370 sb.append(" <processorStats>\n"); 371 MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer(); 372 if (server != null) { 373 // get all the processor mbeans and sort them accordingly to their index 374 String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : ""; 375 ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*"); 376 Set<ObjectName> names = server.queryNames(query, null); 377 List<ManagedProcessorMBean> mps = new ArrayList<ManagedProcessorMBean>(); 378 for (ObjectName on : names) { 379 ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedProcessorMBean.class); 380 381 // the processor must belong to this route 382 if (getRouteId().equals(processor.getRouteId())) { 383 mps.add(processor); 384 } 385 } 386 mps.sort(new OrderProcessorMBeans()); 387 388 // walk the processors in reverse order, and calculate the accumulated total time 389 Map<String, Long> accumulatedTimes = new HashMap<String, Long>(); 390 Collections.reverse(mps); 391 for (ManagedProcessorMBean processor : mps) { 392 processorAccumulatedTime += processor.getTotalProcessingTime(); 393 accumulatedTimes.put(processor.getProcessorId(), processorAccumulatedTime); 394 } 395 // and reverse back again 396 Collections.reverse(mps); 397 398 // and now add the sorted list of processors to the xml output 399 for (ManagedProcessorMBean processor : mps) { 400 sb.append(" <processorStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", processor.getProcessorId(), processor.getIndex(), processor.getState())); 401 // do we have an accumulated time then append that 402 Long accTime = accumulatedTimes.get(processor.getProcessorId()); 403 if (accTime != null) { 404 sb.append(" accumulatedProcessingTime=\"").append(accTime).append("\""); 405 } 406 // use substring as we only want the attributes 407 sb.append(" ").append(processor.dumpStatsAsXml(fullStats).substring(7)).append("\n"); 408 } 409 } 410 sb.append(" </processorStats>\n"); 411 } 412 413 // route self time is route total - processor accumulated total) 414 long routeSelfTime = getTotalProcessingTime() - processorAccumulatedTime; 415 if (routeSelfTime < 0) { 416 // ensure we don't calculate that as negative 417 routeSelfTime = 0; 418 } 419 420 StringBuilder answer = new StringBuilder(); 421 answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState())); 422 // use substring as we only want the attributes 423 String stat = dumpStatsAsXml(fullStats); 424 answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\""); 425 answer.append(" selfProcessingTime=\"").append(routeSelfTime).append("\""); 426 InflightRepository.InflightExchange oldest = getOldestInflightEntry(); 427 if (oldest == null) { 428 answer.append(" oldestInflightExchangeId=\"\""); 429 answer.append(" oldestInflightDuration=\"\""); 430 } else { 431 answer.append(" oldestInflightExchangeId=\"").append(oldest.getExchange().getExchangeId()).append("\""); 432 answer.append(" oldestInflightDuration=\"").append(oldest.getDuration()).append("\""); 433 } 434 answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n"); 435 436 if (includeProcessors) { 437 answer.append(sb); 438 } 439 440 answer.append("</routeStat>"); 441 return answer.toString(); 442 } 443 444 public void reset(boolean includeProcessors) throws Exception { 445 reset(); 446 447 // and now reset all processors for this route 448 if (includeProcessors) { 449 MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer(); 450 if (server != null) { 451 // get all the processor mbeans and sort them accordingly to their index 452 String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : ""; 453 ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*"); 454 QueryExp queryExp = Query.match(new AttributeValueExp("RouteId"), new StringValueExp(getRouteId())); 455 Set<ObjectName> names = server.queryNames(query, queryExp); 456 for (ObjectName name : names) { 457 server.invoke(name, "reset", null, null); 458 } 459 } 460 } 461 } 462 463 public String createRouteStaticEndpointJson() { 464 return getContext().createRouteStaticEndpointJson(getRouteId()); 465 } 466 467 @Override 468 public String createRouteStaticEndpointJson(boolean includeDynamic) { 469 return getContext().createRouteStaticEndpointJson(getRouteId(), includeDynamic); 470 } 471 472 @Override 473 public boolean equals(Object o) { 474 return this == o || (o != null && getClass() == o.getClass() && route.equals(((ManagedRoute) o).route)); 475 } 476 477 @Override 478 public int hashCode() { 479 return route.hashCode(); 480 } 481 482 private InflightRepository.InflightExchange getOldestInflightEntry() { 483 return getContext().getInflightRepository().oldest(getRouteId()); 484 } 485 486 public Long getOldestInflightDuration() { 487 InflightRepository.InflightExchange oldest = getOldestInflightEntry(); 488 if (oldest == null) { 489 return null; 490 } else { 491 return oldest.getDuration(); 492 } 493 } 494 495 public String getOldestInflightExchangeId() { 496 InflightRepository.InflightExchange oldest = getOldestInflightEntry(); 497 if (oldest == null) { 498 return null; 499 } else { 500 return oldest.getExchange().getExchangeId(); 501 } 502 } 503 504 @Override 505 public Boolean getHasRouteController() { 506 return route.getRouteContext().getRouteController() != null; 507 } 508 509 @Override 510 public RouteError getLastError() { 511 return route.getRouteContext().getLastError(); 512 } 513 514 /** 515 * Used for sorting the processor mbeans accordingly to their index. 516 */ 517 private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> { 518 519 @Override 520 public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) { 521 return o1.getIndex().compareTo(o2.getIndex()); 522 } 523 } 524}