1 /*
   2  * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jfr;
  27 
  28 import java.io.Closeable;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.nio.file.Path;
  32 import java.time.Duration;
  33 import java.time.Instant;
  34 import java.util.Collections;
  35 import java.util.HashMap;
  36 import java.util.Map;
  37 import java.util.Objects;
  38 
  39 import jdk.jfr.internal.PlatformRecording;
  40 import jdk.jfr.internal.Type;
  41 import jdk.jfr.internal.Utils;
  42 import jdk.jfr.internal.WriteableUserPath;
  43 
  44 /**
  45  * Provides means to configure, start, stop and dump recording data to disk.
  46  * <p>
  47  * Example,
  48  *
  49  * <pre>
  50  * <code>
  51  *   Configuration c = Configuration.getConfiguration("default");
  52  *   Recording r = new Recording(c);
  53  *   r.start();
  54  *   System.gc();
  55  *   Thread.sleep(5000);
  56  *   r.stop();
  57  *   r.copyTo(Files.createTempFile("my-recording", ".jfr"));
  58  * </code>
  59  * </pre>
  60  *
  61  * @since 9
  62  */
  63 public final class Recording implements Closeable {
  64 
  65     private static class RecordingSettings extends EventSettings {
  66 
  67         private final Recording recording;
  68         private final String identifier;
  69 
  70         RecordingSettings(Recording r, String identifier) {
  71             this.recording = r;
  72             this.identifier = identifier;
  73         }
  74 
  75         RecordingSettings(Recording r, Class<? extends Event> eventClass) {
  76             Utils.ensureValidEventSubclass(eventClass);
  77             this.recording = r;
  78             this.identifier = String.valueOf(Type.getTypeId(eventClass));
  79         }
  80 
  81         @Override
  82         public EventSettings with(String name, String value) {
  83             Objects.requireNonNull(value);
  84             recording.setSetting(identifier + "#" + name, value);
  85             return this;
  86         }
  87 
  88         @Override
  89         public Map<String, String> toMap() {
  90             return recording.getSettings();
  91         }
  92     }
  93 
  94     private final PlatformRecording internal;
  95 
  96     private Recording(PlatformRecording internal) {
  97         this.internal = internal;
  98         this.internal.setRecording(this);
  99         if (internal.getRecording() != this) {
 100             throw new InternalError("Internal recording not properly setup");
 101         }
 102     }
 103 
 104     /**
 105      * Creates a recording without any settings.
 106      * <p>
 107      * A newly created recording will be in {@link RecordingState#NEW}. To start
 108      * the recording invoke {@link Recording#start()}.
 109      *
 110      * @throws IllegalStateException if the platform Flight Recorder couldn't be
 111      *         created, for instance if commercial features isn't unlocked, or
 112      *         if the file repository can't be created or accessed.
 113      *
 114      * @throws SecurityException If a security manager is used and
 115      *         FlightRecorderPermission "accessFlightRecorder" is not set.
 116      */
 117     public Recording() {
 118         this(FlightRecorder.getFlightRecorder().newInternalRecording(new HashMap<String, String>()));
 119     }
 120 
 121     /**
 122      * Creates a recording with settings from a configuration.
 123      * <p>
 124      * Example,
 125      *
 126      * <pre>
 127      * <code>
 128      * Recording r = new Recording(Configuration.getConfiguration("default"));
 129      * </code>
 130      * </pre>
 131      *
 132      * The newly created recording will be in {@link RecordingState#NEW}. To
 133      * start the recording invoke {@link Recording#start()}.
 134      *
 135      * @param configuration configuration containing settings to be used, not
 136      *        {@code null}
 137      *
 138      * @throws IllegalStateException if the platform Flight Recorder couldn't be
 139      *         created, for instance if commercial features isn't unlocked, or
 140      *         if the file repository can't be created or accessed.
 141      *
 142      * @throws SecurityException If a security manager is used and
 143      *         FlightRecorderPermission "accessFlightRecorder" is not set.
 144      *
 145      * @see Configuration
 146      */
 147     public Recording(Configuration configuration) {
 148         this(FlightRecorder.getFlightRecorder().newInternalRecording(configuration.getSettings()));
 149     }
 150 
 151     /**
 152      * Starts this recording.
 153      * <p>
 154      * It's recommended that recording options and event settings are configured
 155      * before calling this method, since it guarantees a more consistent state
 156      * when analyzing the recorded data. It may also have performance benefits,
 157      * since the configuration can be applied atomically.
 158      * <p>
 159      * After a successful invocation of this method, the state of this recording will be
 160      * in {@code RUNNING} state.
 161      *
 162      * @throws IllegalStateException if recording has already been started or is
 163      *         in {@code CLOSED} state
 164      */
 165     public void start() {
 166         internal.start();
 167     }
 168 
 169     /**
 170      * Starts this recording after a delay.
 171      * <p>
 172      * After a successful invocation of this method, the state of this recording will be
 173      * in the {@code DELAYED} state.
 174      *
 175      * @param delay the time to wait before starting this recording, not
 176      *        {@code null}
 177      * @throws IllegalStateException if already started or is in {@code CLOSED}
 178      *         state
 179      */
 180     public void scheduleStart(Duration delay) {
 181         Objects.requireNonNull(delay);
 182         internal.scheduleStart(delay);
 183     }
 184 
 185     /**
 186      * Stops this recording.
 187      * <p>
 188      * Once a recording has been stopped it can't be restarted. If this
 189      * recording has a destination, data will be written to that destination and
 190      * the recording closed. Once a recording is closed, the data is no longer
 191      * available.
 192      * <p>
 193      * After a successful invocation of this method, the state of this recording will be
 194      * in {@code STOPPED} state.
 195      *
 196      * @return {@code true} if recording was stopped, {@code false} otherwise
 197      *
 198      * @throws IllegalStateException if not started or already been stopped
 199      *
 200      * @throws SecurityException if a security manager exists and the caller
 201      *         doesn't have {@code FilePermission} to write at the destination
 202      *         path
 203      *
 204      * @see #setDestination(Path)
 205      *
 206      */
 207     public boolean stop() {
 208         return internal.stop("Stopped by user");
 209     }
 210 
 211     /**
 212      * Returns settings used by this recording.
 213      * <p>
 214      * Modifying the returned map will not change settings for this recording.
 215      * <p>
 216      * If no settings have been set for this recording, an empty map is
 217      * returned.
 218      *
 219      * @return recording settings, not {@code null}
 220      */
 221     public Map<String, String> getSettings() {
 222         return new HashMap<>(internal.getSettings());
 223     }
 224 
 225     /**
 226      * Returns the current size of this recording in the disk repository,
 227      * measured in bytes.
 228      *
 229      * Size is updated when recording buffers are flushed. If the recording is
 230      * not to disk the returned is always {@code 0}.
 231      *
 232      * @return amount of recorded data, measured in bytes, or {@code 0} if the
 233      *         recording is not to disk
 234      */
 235     public long getSize() {
 236         return internal.getSize();
 237     }
 238 
 239     /**
 240      * Returns the time when this recording was stopped.
 241      *
 242      * @return the the time, or {@code null} if this recording hasn't been
 243      *         stopped
 244      */
 245     public Instant getStopTime() {
 246         return internal.getStopTime();
 247     }
 248 
 249     /**
 250      * Returns the time when this recording was started.
 251      *
 252      * @return the the time, or {@code null} if this recording hasn't been
 253      *         started
 254      */
 255     public Instant getStartTime() {
 256         return internal.getStartTime();
 257     }
 258 
 259     /**
 260      * Returns the max size, measured in bytes, at which data will no longer be
 261      * kept in the disk repository.
 262      *
 263      * @return max size in bytes, or {@code 0} if no max size has been set
 264      */
 265     public long getMaxSize() {
 266         return internal.getMaxSize();
 267     }
 268 
 269     /**
 270      * Returns how long data is to be kept in the disk repository before it is
 271      * thrown away.
 272      *
 273      * @return max age, or {@code null} if no maximum age has been set
 274      */
 275     public Duration getMaxAge() {
 276         return internal.getMaxAge();
 277     }
 278 
 279     /**
 280      * Returns the name of this recording.
 281      * <p>
 282      * By default the name is the same as the recording id.
 283      *
 284      * @return recording name, not {@code null}
 285      */
 286     public String getName() {
 287         return internal.getName();
 288     }
 289 
 290     /**
 291      * Replaces all settings for this recording,
 292      * <p>
 293      * Example,
 294      *
 295      * <pre>
 296      * <code>
 297      *     Map{@literal <}String, String{@literal >} settings = new HashMap{@literal <}{@literal >}();
 298      *     settings.putAll(EventSettings.enabled("com.oracle.jdk.CPUSample").withPeriod(Duration.ofSeconds(2)).toMap());
 299      *     settings.putAll(EventSettings.enabled(MyEvent.class).withThreshold(Duration.ofSeconds(2)).withoutStackTrace().toMap());
 300      *     settings.put("com.oracle.jdk.ExecutionSample#period", "10 ms");
 301      *     recording.setSettings(settings);
 302      * </code>
 303      * </pre>
 304      *
 305      * To merge with existing settings do.
 306      *
 307      * <pre>
 308      * {@code
 309      * Map<String, String> settings = recording.getSettings();
 310      * settings.putAll(additionalSettings));
 311      * recording.setSettings(settings);
 312      * }
 313      * </pre>
 314      *
 315      * @param settings the settings to set, not {@code null}
 316      */
 317     public void setSettings(Map<String, String> settings) {
 318         Objects.requireNonNull(settings);
 319         Map<String, String> sanitized = Utils.sanitizeNullFreeStringMap(settings);
 320         internal.setSettings(sanitized);
 321     }
 322 
 323     /**
 324      * Returns the <code>RecordingState</code> this recording is currently in.
 325      *
 326      * @return the recording state, not {@code null}
 327      *
 328      * @see RecordingState
 329      */
 330     public RecordingState getState() {
 331         return internal.getState();
 332     }
 333 
 334     /**
 335      * Releases all data associated with this recording.
 336      * <p>
 337      * After a successful invocation of this method, the state of this recording will be
 338      * in the {@code CLOSED} state.
 339      */
 340     @Override
 341     public void close() {
 342         internal.close();
 343     }
 344 
 345     /**
 346      * Returns a clone of this recording, but with a new recording id and name.
 347      *
 348      * Clones are useful for dumping data without stopping the recording. After
 349      * a clone is created the amount of data to be copied out can be constrained
 350      * with the {@link #setMaxAge(Duration)} and {@link #setMaxSize(long)}.
 351      *
 352      * @param stop {@code true} if the newly created copy should be stopped
 353      *        immediately, {@code false} otherwise
 354      * @return the recording copy, not {@code null}
 355      */
 356     public Recording copy(boolean stop) {
 357         return internal.newCopy(stop);
 358     }
 359 
 360     /**
 361      * Writes recording data to a file.
 362      * <p>
 363      * Recording must be started, but not necessarily stopped.
 364      *
 365      * @param destination where recording data should be written, not
 366      *        {@code null}
 367      *
 368      * @throws IOException if recording can't be copied to {@code path}
 369      *
 370      * @throws SecurityException if a security manager exists and the caller
 371      *         doesn't have {@code FilePermission} to write at the destination
 372      *         path
 373      */
 374     public void dump(Path destination) throws IOException {
 375         Objects.requireNonNull(destination);
 376         internal.copyTo(new WriteableUserPath(destination), "Dumped by user", Collections.emptyMap());
 377     }
 378 
 379     /**
 380      * Returns if this recording is to disk.
 381      * <p>
 382      * If no value has been set, {@code true} is returned.
 383      *
 384      * @return {@code true} if recording is to disk, {@code false} otherwise
 385      */
 386     public boolean isToDisk() {
 387         return internal.isToDisk();
 388     }
 389 
 390     /**
 391      * Determines how much data should be kept in disk repository.
 392      * <p>
 393      * In order to control the amount of recording data stored on disk, and not
 394      * having it grow indefinitely, Max size can be used to limit the amount of
 395      * data retained. When the {@code maxSize} limit has been exceeded, the JVM
 396      * will remove the oldest chunk to make room for a more recent chunk.
 397      *
 398      * @param maxSize or {@code 0} if infinite
 399      *
 400      * @throws IllegalArgumentException if <code>maxSize</code> is negative
 401      *
 402      * @throws IllegalStateException if the recording is in {@code CLOSED} state
 403      */
 404     public void setMaxSize(long maxSize) {
 405         if (maxSize < 0) {
 406             throw new IllegalArgumentException("Max size of recording can't be negative");
 407         }
 408         internal.setMaxSize(maxSize);
 409     }
 410 
 411     /**
 412      * Determines how far back data should be kept in disk repository.
 413      * <p>
 414      * In order to control the amount of recording data stored on disk, and not
 415      * having it grow indefinitely, {@code maxAge} can be used to limit the
 416      * amount of data retained. Data stored on disk which is older than
 417      * {@code maxAge} will be removed by Flight Recorder.
 418      *
 419      * @param maxAge how long data can be kept, or {@code null} if infinite
 420      *
 421      * @throws IllegalArgumentException if <code>maxAge</code> is negative
 422      *
 423      * @throws IllegalStateException if the recording is in closed state
 424      */
 425     public void setMaxAge(Duration maxAge) {
 426         if (maxAge != null && maxAge.isNegative()) {
 427             throw new IllegalArgumentException("Max age of recording can't be negative");
 428         }
 429         internal.setMaxAge(maxAge);
 430     }
 431 
 432     /**
 433      * Sets a location where data will be written on recording stop, or
 434      * {@code null} if data should not be dumped automatically.
 435      * <p>
 436      * If a destination is set, this recording will be closed automatically
 437      * after data has been copied successfully to the destination path.
 438      * <p>
 439      * If a destination is <em>not</em> set, Flight Recorder will hold on to
 440      * recording data until this recording is closed. Use {@link #dump(Path)} to
 441      * write data to a file manually.
 442      *
 443      * @param destination destination path, or {@code null} if recording should
 444      *        not be dumped at stop
 445      *
 446      * @throws IllegalStateException if recording is in {@code ETOPPED} or
 447      *         {@code CLOSED} state.
 448      *
 449      * @throws SecurityException if a security manager exists and the caller
 450      *         doesn't have {@code FilePermission} to read, write and delete the
 451      *         {@code destination} file
 452      *
 453      * @throws IOException if path is not writable
 454      */
 455     public void setDestination(Path destination) throws IOException {
 456         internal.setDestination(destination != null ? new WriteableUserPath(destination) : null);
 457     }
 458 
 459     /**
 460      * Returns destination file, where recording data will be written when the
 461      * recording stops, or {@code null} if no destination has been set.
 462      *
 463      * @return the destination file, or {@code null} if not set.
 464      */
 465     public Path getDestination() {
 466         WriteableUserPath usp = internal.getDestination();
 467         if (usp == null) {
 468             return null;
 469         } else {
 470             return usp.getPotentiallyMaliciousOriginal();
 471         }
 472     }
 473 
 474     /**
 475      * Returns a unique identifier for this recording.
 476      *
 477      * @return the recording identifier
 478      */
 479     public long getId() {
 480         return internal.getId();
 481     }
 482 
 483     /**
 484      * Sets a human-readable name, such as {@code "My Recording"}.
 485      *
 486      * @param name the recording name, not {@code null}
 487      *
 488      * @throws IllegalStateException if the recording is in closed state
 489      */
 490     public void setName(String name) {
 491         Objects.requireNonNull(name);
 492         internal.setName(name);
 493     }
 494 
 495     /**
 496      * Sets if this recording should be dumped to disk when the JVM exits.
 497      *
 498      * @param dumpOnExit if recording should be dumped on JVM exit
 499      */
 500     public void setDumpOnExit(boolean dumpOnExit) {
 501         internal.setDumpOnExit(dumpOnExit);
 502     }
 503 
 504     /**
 505      * Returns if this recording should be dumped to disk when the JVM exits.
 506      * <p>
 507      * If dump on exit has not been set, {@code false} is returned.
 508      *
 509      * @return {@code true} if recording should be dumped on exit, {@code false}
 510      *         otherwise.
 511      */
 512     public boolean getDumpOnExit() {
 513         return internal.getDumpOnExit();
 514     }
 515 
 516     /**
 517      * Determines if this recording should be flushed to disk continuously or if
 518      * data should be constrained to what is available in memory buffers.
 519      *
 520      * @param disk {@code true} if recording should be written to disk,
 521      *        {@code false} if in-memory
 522      *
 523      */
 524     public void setToDisk(boolean disk) {
 525         internal.setToDisk(disk);
 526     }
 527 
 528     /**
 529      * Creates a data stream for a specified interval.
 530      *
 531      * The stream may contain some data outside the specified range.
 532      *
 533      * @param start start time for the stream, or {@code null} to get data from
 534      *        start time of the recording
 535      *
 536      * @param end end time for the stream, or {@code null} to get data until the
 537      *        present time.
 538      *
 539      * @return an input stream, or {@code null} if no data is available in the
 540      *         interval.
 541      *
 542      * @throws IllegalArgumentException if {@code end} happens before
 543      *         {@code start}
 544      *
 545      * @throws IOException if a stream can't be opened
 546      */
 547     public InputStream getStream(Instant start, Instant end) throws IOException {
 548         if (start != null && end != null && end.isBefore(start)) {
 549             throw new IllegalArgumentException("End time of requested stream must not be before start time");
 550         }
 551         return internal.open(start, end);
 552     }
 553 
 554     /**
 555      * Returns the desired duration for this recording, or {@code null} if no
 556      * duration has been set.
 557      * <p>
 558      * The duration can only be set when the recording state is
 559      * {@link RecordingState#NEW}.
 560      *
 561      * @return the desired duration of the recording, or {@code null} if no
 562      *         duration has been set.
 563      */
 564     public Duration getDuration() {
 565         return internal.getDuration();
 566     }
 567 
 568     /**
 569      * Sets a duration for how long a recording should run before it's stopped.
 570      * <p>
 571      * By default a recording has no duration (<code>null</code>).
 572      *
 573      * @param duration the duration or {@code null} if no duration should be
 574      *        used
 575      *
 576      * @throws IllegalStateException if recording is in stopped or closed state
 577      */
 578     public void setDuration(Duration duration) {
 579         internal.setDuration(duration);
 580     }
 581 
 582     /**
 583      * Enables the event with the specified name.
 584      * <p>
 585      * If there are multiple events with same name, which can be the case if the
 586      * same class is loaded in different class loaders, all events matching the
 587      * name will be enabled. To enable a specific class, use the
 588      * {@link #enable(Class)} method or a String representation of the event
 589      * type id.
 590      *
 591      * @param name the settings for the event, not {@code null}
 592      *
 593      * @return an event setting for further configuration, not {@code null}
 594      *
 595      * @see EventType
 596      */
 597     public EventSettings enable(String name) {
 598         Objects.requireNonNull(name);
 599         RecordingSettings rs = new RecordingSettings(this, name);
 600         rs.with("enabled", "true");
 601         return rs;
 602     }
 603 
 604     /**
 605      * Disables event with the specified name.
 606      * <p>
 607      * If there are multiple events with same name, which can be the case if the
 608      * same class is loaded in different class loaders, all events matching the
 609      * name will be disabled. To disable a specific class, use the
 610      * {@link #disable(Class)} method or a String representation of the event
 611      * type id.
 612      *
 613      * @param name the settings for the event, not {@code null}
 614      *
 615      * @return an event setting for further configuration, not {@code null}
 616      *
 617      */
 618     public EventSettings disable(String name) {
 619         Objects.requireNonNull(name);
 620         RecordingSettings rs = new RecordingSettings(this, name);
 621         rs.with("enabled", "false");
 622         return rs;
 623     }
 624 
 625     /**
 626      * Enables event.
 627      *
 628      * @param eventClass the event to enable, not {@code null}
 629      *
 630      * @throws IllegalArgumentException if {@code eventClass} is an abstract
 631      *         class or not a subclass of {@link Event}
 632      *
 633      * @return an event setting for further configuration, not {@code null}
 634      */
 635     public EventSettings enable(Class<? extends Event> eventClass) {
 636         Objects.requireNonNull(eventClass);
 637         RecordingSettings rs = new RecordingSettings(this, eventClass);
 638         rs.with("enabled", "true");
 639         return rs;
 640     }
 641 
 642     /**
 643      * Disables event.
 644      *
 645      * @param eventClass the event to enable, not {@code null}
 646      *
 647      * @throws IllegalArgumentException if {@code eventClass} is an abstract
 648      *         class or not a subclass of {@link Event}
 649      *
 650      * @return an event setting for further configuration, not {@code null}
 651      *
 652      */
 653     public EventSettings disable(Class<? extends Event> eventClass) {
 654         Objects.requireNonNull(eventClass);
 655         RecordingSettings rs = new RecordingSettings(this, eventClass);
 656         rs.with("enabled", "false");
 657         return rs;
 658     }
 659 
 660     // package private
 661     PlatformRecording getInternal() {
 662         return internal;
 663     }
 664 
 665     private void setSetting(String id, String value) {
 666         Objects.requireNonNull(id);
 667         Objects.requireNonNull(value);
 668         internal.setSetting(id, value);
 669     }
 670 
 671 }