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