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