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