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