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.ConcurrentHashMap; 029import java.util.concurrent.ConcurrentSkipListMap; 030import java.util.concurrent.TimeUnit; 031import java.util.concurrent.atomic.AtomicBoolean; 032import javax.management.AttributeValueExp; 033import javax.management.MBeanServer; 034import javax.management.ObjectName; 035import javax.management.Query; 036import javax.management.QueryExp; 037import javax.management.StringValueExp; 038 039import org.w3c.dom.Document; 040 041import org.apache.camel.CamelContext; 042import org.apache.camel.Exchange; 043import org.apache.camel.ManagementStatisticsLevel; 044import org.apache.camel.Route; 045import org.apache.camel.ServiceStatus; 046import org.apache.camel.TimerListener; 047import org.apache.camel.api.management.ManagedResource; 048import org.apache.camel.api.management.mbean.ManagedProcessorMBean; 049import org.apache.camel.api.management.mbean.ManagedRouteMBean; 050import org.apache.camel.model.ModelCamelContext; 051import org.apache.camel.model.ModelHelper; 052import org.apache.camel.model.RouteDefinition; 053import org.apache.camel.spi.ManagementStrategy; 054import org.apache.camel.spi.RoutePolicy; 055import org.apache.camel.util.ObjectHelper; 056import org.apache.camel.util.XmlLineNumberParser; 057import org.slf4j.Logger; 058import org.slf4j.LoggerFactory; 059 060@ManagedResource(description = "Managed Route") 061public class ManagedRoute extends ManagedPerformanceCounter implements TimerListener, ManagedRouteMBean { 062 063 public static final String VALUE_UNKNOWN = "Unknown"; 064 065 private static final Logger LOG = LoggerFactory.getLogger(ManagedRoute.class); 066 067 protected final Route route; 068 protected final String description; 069 protected final ModelCamelContext context; 070 private final LoadTriplet load = new LoadTriplet(); 071 private final ConcurrentSkipListMap<InFlightKey, Long> exchangesInFlightStartTimestamps = new ConcurrentSkipListMap<InFlightKey, Long>(); 072 private final ConcurrentHashMap<String, InFlightKey> exchangesInFlightKeys = new ConcurrentHashMap<String, InFlightKey>(); 073 074 public ManagedRoute(ModelCamelContext context, Route route) { 075 this.route = route; 076 this.context = context; 077 this.description = route.getDescription(); 078 } 079 080 @Override 081 public void init(ManagementStrategy strategy) { 082 super.init(strategy); 083 boolean enabled = context.getManagementStrategy().getManagementAgent().getStatisticsLevel() != ManagementStatisticsLevel.Off; 084 setStatisticsEnabled(enabled); 085 086 exchangesInFlightKeys.clear(); 087 exchangesInFlightStartTimestamps.clear(); 088 } 089 090 public Route getRoute() { 091 return route; 092 } 093 094 public CamelContext getContext() { 095 return context; 096 } 097 098 public String getRouteId() { 099 String id = route.getId(); 100 if (id == null) { 101 id = VALUE_UNKNOWN; 102 } 103 return id; 104 } 105 106 public String getDescription() { 107 return description; 108 } 109 110 @Override 111 public String getEndpointUri() { 112 if (route.getEndpoint() != null) { 113 return route.getEndpoint().getEndpointUri(); 114 } 115 return VALUE_UNKNOWN; 116 } 117 118 public String getState() { 119 // must use String type to be sure remote JMX can read the attribute without requiring Camel classes. 120 ServiceStatus status = context.getRouteStatus(route.getId()); 121 // if no status exists then its stopped 122 if (status == null) { 123 status = ServiceStatus.Stopped; 124 } 125 return status.name(); 126 } 127 128 public String getUptime() { 129 return route.getUptime(); 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 String getRoutePolicyList() { 157 List<RoutePolicy> policyList = route.getRouteContext().getRoutePolicyList(); 158 159 if (policyList == null || policyList.isEmpty()) { 160 // return an empty string to have it displayed nicely in JMX consoles 161 return ""; 162 } 163 164 StringBuilder sb = new StringBuilder(); 165 for (int i = 0; i < policyList.size(); i++) { 166 RoutePolicy policy = policyList.get(i); 167 sb.append(policy.getClass().getSimpleName()); 168 sb.append("(").append(ObjectHelper.getIdentityHashCode(policy)).append(")"); 169 if (i < policyList.size() - 1) { 170 sb.append(", "); 171 } 172 } 173 return sb.toString(); 174 } 175 176 public String getLoad01() { 177 double load1 = load.getLoad1(); 178 if (Double.isNaN(load1)) { 179 // empty string if load statistics is disabled 180 return ""; 181 } else { 182 return String.format("%.2f", load1); 183 } 184 } 185 186 public String getLoad05() { 187 double load5 = load.getLoad5(); 188 if (Double.isNaN(load5)) { 189 // empty string if load statistics is disabled 190 return ""; 191 } else { 192 return String.format("%.2f", load5); 193 } 194 } 195 196 public String getLoad15() { 197 double load15 = load.getLoad15(); 198 if (Double.isNaN(load15)) { 199 // empty string if load statistics is disabled 200 return ""; 201 } else { 202 return String.format("%.2f", load15); 203 } 204 } 205 206 @Override 207 public void onTimer() { 208 load.update(getInflightExchanges()); 209 } 210 211 public void start() throws Exception { 212 if (!context.getStatus().isStarted()) { 213 throw new IllegalArgumentException("CamelContext is not started"); 214 } 215 context.startRoute(getRouteId()); 216 } 217 218 public void stop() throws Exception { 219 if (!context.getStatus().isStarted()) { 220 throw new IllegalArgumentException("CamelContext is not started"); 221 } 222 context.stopRoute(getRouteId()); 223 } 224 225 public void stop(long timeout) throws Exception { 226 if (!context.getStatus().isStarted()) { 227 throw new IllegalArgumentException("CamelContext is not started"); 228 } 229 context.stopRoute(getRouteId(), timeout, TimeUnit.SECONDS); 230 } 231 232 public boolean stop(Long timeout, Boolean abortAfterTimeout) throws Exception { 233 if (!context.getStatus().isStarted()) { 234 throw new IllegalArgumentException("CamelContext is not started"); 235 } 236 return context.stopRoute(getRouteId(), timeout, TimeUnit.SECONDS, abortAfterTimeout); 237 } 238 239 public void shutdown() throws Exception { 240 if (!context.getStatus().isStarted()) { 241 throw new IllegalArgumentException("CamelContext is not started"); 242 } 243 String routeId = getRouteId(); 244 context.stopRoute(routeId); 245 context.removeRoute(routeId); 246 } 247 248 public void shutdown(long timeout) throws Exception { 249 if (!context.getStatus().isStarted()) { 250 throw new IllegalArgumentException("CamelContext is not started"); 251 } 252 String routeId = getRouteId(); 253 context.stopRoute(routeId, timeout, TimeUnit.SECONDS); 254 context.removeRoute(routeId); 255 } 256 257 public boolean remove() throws Exception { 258 if (!context.getStatus().isStarted()) { 259 throw new IllegalArgumentException("CamelContext is not started"); 260 } 261 return context.removeRoute(getRouteId()); 262 } 263 264 public String dumpRouteAsXml() throws Exception { 265 return dumpRouteAsXml(false); 266 } 267 268 @Override 269 public String dumpRouteAsXml(boolean resolvePlaceholders) throws Exception { 270 String id = route.getId(); 271 RouteDefinition def = context.getRouteDefinition(id); 272 if (def != null) { 273 String xml = ModelHelper.dumpModelAsXml(context, def); 274 275 // if resolving placeholders we parse the xml, and resolve the property placeholders during parsing 276 if (resolvePlaceholders) { 277 final AtomicBoolean changed = new AtomicBoolean(); 278 InputStream is = new ByteArrayInputStream(xml.getBytes()); 279 Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() { 280 @Override 281 public String transform(String text) { 282 try { 283 String after = getContext().resolvePropertyPlaceholders(text); 284 if (!changed.get()) { 285 changed.set(!text.equals(after)); 286 } 287 return after; 288 } catch (Exception e) { 289 // ignore 290 return text; 291 } 292 } 293 }); 294 // okay there were some property placeholder replaced so re-create the model 295 if (changed.get()) { 296 xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom); 297 RouteDefinition copy = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class); 298 xml = ModelHelper.dumpModelAsXml(context, copy); 299 } 300 } 301 return xml; 302 } 303 return null; 304 } 305 306 public void updateRouteFromXml(String xml) throws Exception { 307 // convert to model from xml 308 RouteDefinition def = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class); 309 if (def == null) { 310 return; 311 } 312 313 // if the xml does not contain the route-id then we fix this by adding the actual route id 314 // this may be needed if the route-id was auto-generated, as the intend is to update this route 315 // and not add a new route, adding a new route, use the MBean operation on ManagedCamelContext instead. 316 if (ObjectHelper.isEmpty(def.getId())) { 317 def.setId(getRouteId()); 318 } else if (!def.getId().equals(getRouteId())) { 319 throw new IllegalArgumentException("Cannot update route from XML as routeIds does not match. routeId: " 320 + getRouteId() + ", routeId from XML: " + def.getId()); 321 } 322 323 LOG.debug("Updating route: {} from xml: {}", def.getId(), xml); 324 325 try { 326 // add will remove existing route first 327 context.addRouteDefinition(def); 328 } catch (Exception e) { 329 // log the error as warn as the management api may be invoked remotely over JMX which does not propagate such exception 330 String msg = "Error updating route: " + def.getId() + " from xml: " + xml + " due: " + e.getMessage(); 331 LOG.warn(msg, e); 332 throw e; 333 } 334 } 335 336 public String dumpRouteStatsAsXml(boolean fullStats, boolean includeProcessors) throws Exception { 337 // in this logic we need to calculate the accumulated processing time for the processor in the route 338 // and hence why the logic is a bit more complicated to do this, as we need to calculate that from 339 // the bottom -> top of the route but this information is valuable for profiling routes 340 StringBuilder sb = new StringBuilder(); 341 342 // need to calculate this value first, as we need that value for the route stat 343 Long processorAccumulatedTime = 0L; 344 345 // gather all the processors for this route, which requires JMX 346 if (includeProcessors) { 347 sb.append(" <processorStats>\n"); 348 MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer(); 349 if (server != null) { 350 // get all the processor mbeans and sort them accordingly to their index 351 String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : ""; 352 ObjectName query = ObjectName.getInstance("org.apache.camel:context=" + prefix + getContext().getManagementName() + ",type=processors,*"); 353 Set<ObjectName> names = server.queryNames(query, null); 354 List<ManagedProcessorMBean> mps = new ArrayList<ManagedProcessorMBean>(); 355 for (ObjectName on : names) { 356 ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedProcessorMBean.class); 357 358 // the processor must belong to this route 359 if (getRouteId().equals(processor.getRouteId())) { 360 mps.add(processor); 361 } 362 } 363 Collections.sort(mps, new OrderProcessorMBeans()); 364 365 // walk the processors in reverse order, and calculate the accumulated total time 366 Map<String, Long> accumulatedTimes = new HashMap<String, Long>(); 367 Collections.reverse(mps); 368 for (ManagedProcessorMBean processor : mps) { 369 processorAccumulatedTime += processor.getTotalProcessingTime(); 370 accumulatedTimes.put(processor.getProcessorId(), processorAccumulatedTime); 371 } 372 // and reverse back again 373 Collections.reverse(mps); 374 375 // and now add the sorted list of processors to the xml output 376 for (ManagedProcessorMBean processor : mps) { 377 sb.append(" <processorStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", processor.getProcessorId(), processor.getIndex(), processor.getState())); 378 // do we have an accumulated time then append that 379 Long accTime = accumulatedTimes.get(processor.getProcessorId()); 380 if (accTime != null) { 381 sb.append(" accumulatedProcessingTime=\"").append(accTime).append("\""); 382 } 383 // use substring as we only want the attributes 384 sb.append(" ").append(processor.dumpStatsAsXml(fullStats).substring(7)).append("\n"); 385 } 386 } 387 sb.append(" </processorStats>\n"); 388 } 389 390 // route self time is route total - processor accumulated total) 391 long routeSelfTime = getTotalProcessingTime() - processorAccumulatedTime; 392 if (routeSelfTime < 0) { 393 // ensure we don't calculate that as negative 394 routeSelfTime = 0; 395 } 396 397 StringBuilder answer = new StringBuilder(); 398 answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState())); 399 // use substring as we only want the attributes 400 String stat = dumpStatsAsXml(fullStats); 401 answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\""); 402 answer.append(" selfProcessingTime=\"").append(routeSelfTime).append("\""); 403 InFlightKey oldestInflightEntry = getOldestInflightEntry(); 404 if (oldestInflightEntry == null) { 405 answer.append(" oldestInflightExchangeId=\"\""); 406 answer.append(" oldestInflightDuration=\"\""); 407 } else { 408 answer.append(" oldestInflightExchangeId=\"").append(oldestInflightEntry.exchangeId).append("\""); 409 answer.append(" oldestInflightDuration=\"").append(System.currentTimeMillis() - oldestInflightEntry.timeStamp).append("\""); 410 } 411 answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n"); 412 413 if (includeProcessors) { 414 answer.append(sb); 415 } 416 417 answer.append("</routeStat>"); 418 return answer.toString(); 419 } 420 421 public void reset(boolean includeProcessors) throws Exception { 422 reset(); 423 424 // and now reset all processors for this route 425 if (includeProcessors) { 426 MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer(); 427 if (server != null) { 428 // get all the processor mbeans and sort them accordingly to their index 429 String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : ""; 430 ObjectName query = ObjectName.getInstance("org.apache.camel:context=" + prefix + getContext().getManagementName() + ",type=processors,*"); 431 QueryExp queryExp = Query.match(new AttributeValueExp("RouteId"), new StringValueExp(getRouteId())); 432 Set<ObjectName> names = server.queryNames(query, queryExp); 433 for (ObjectName name : names) { 434 server.invoke(name, "reset", null, null); 435 } 436 } 437 } 438 } 439 440 public String createRouteStaticEndpointJson() { 441 return getContext().createRouteStaticEndpointJson(getRouteId()); 442 } 443 444 @Override 445 public String createRouteStaticEndpointJson(boolean includeDynamic) { 446 return getContext().createRouteStaticEndpointJson(getRouteId(), includeDynamic); 447 } 448 449 @Override 450 public boolean equals(Object o) { 451 return this == o || (o != null && getClass() == o.getClass() && route.equals(((ManagedRoute) o).route)); 452 } 453 454 @Override 455 public int hashCode() { 456 return route.hashCode(); 457 } 458 459 private InFlightKey getOldestInflightEntry() { 460 Map.Entry<InFlightKey, Long> entry = exchangesInFlightStartTimestamps.firstEntry(); 461 if (entry != null) { 462 return entry.getKey(); 463 } 464 return null; 465 } 466 467 public Long getOldestInflightDuration() { 468 InFlightKey oldest = getOldestInflightEntry(); 469 if (oldest == null) { 470 return null; 471 } 472 return System.currentTimeMillis() - oldest.timeStamp; 473 } 474 475 public String getOldestInflightExchangeId() { 476 InFlightKey oldest = getOldestInflightEntry(); 477 if (oldest == null) { 478 return null; 479 } 480 return oldest.exchangeId; 481 } 482 483 @Override 484 public synchronized void processExchange(Exchange exchange) { 485 InFlightKey key = new InFlightKey(System.currentTimeMillis(), exchange.getExchangeId()); 486 InFlightKey oldKey = exchangesInFlightKeys.putIfAbsent(exchange.getExchangeId(), key); 487 // we may already have the exchange being processed so only add to timestamp if its a new exchange 488 // for example when people call the same routes recursive 489 if (oldKey == null) { 490 exchangesInFlightStartTimestamps.put(key, key.timeStamp); 491 } 492 super.processExchange(exchange); 493 } 494 495 @Override 496 public synchronized void completedExchange(Exchange exchange, long time) { 497 InFlightKey key = exchangesInFlightKeys.remove(exchange.getExchangeId()); 498 if (key != null) { 499 exchangesInFlightStartTimestamps.remove(key); 500 } 501 super.completedExchange(exchange, time); 502 } 503 504 @Override 505 public synchronized void failedExchange(Exchange exchange) { 506 InFlightKey key = exchangesInFlightKeys.remove(exchange.getExchangeId()); 507 if (key != null) { 508 exchangesInFlightStartTimestamps.remove(key); 509 } 510 super.failedExchange(exchange); 511 } 512 513 private static class InFlightKey implements Comparable<InFlightKey> { 514 515 private final Long timeStamp; 516 private final String exchangeId; 517 518 InFlightKey(Long timeStamp, String exchangeId) { 519 this.timeStamp = timeStamp; 520 this.exchangeId = exchangeId; 521 } 522 523 @Override 524 public int compareTo(InFlightKey o) { 525 int compare = Long.compare(timeStamp, o.timeStamp); 526 if (compare == 0) { 527 return exchangeId.compareTo(o.exchangeId); 528 } 529 return compare; 530 } 531 532 @Override 533 public boolean equals(Object o) { 534 if (this == o) { 535 return true; 536 } 537 if (o == null || getClass() != o.getClass()) { 538 return false; 539 } 540 541 InFlightKey that = (InFlightKey) o; 542 543 if (!exchangeId.equals(that.exchangeId)) { 544 return false; 545 } 546 if (!timeStamp.equals(that.timeStamp)) { 547 return false; 548 } 549 550 return true; 551 } 552 553 @Override 554 public int hashCode() { 555 int result = timeStamp.hashCode(); 556 result = 31 * result + exchangeId.hashCode(); 557 return result; 558 } 559 560 @Override 561 public String toString() { 562 return exchangeId; 563 } 564 } 565 566 /** 567 * Used for sorting the processor mbeans accordingly to their index. 568 */ 569 private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> { 570 571 @Override 572 public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) { 573 return o1.getIndex().compareTo(o2.getIndex()); 574 } 575 } 576}