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