├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── TODO.txt ├── build.sh ├── config.yml ├── defaults.sample.yml ├── examples ├── AD-Insurance.yml ├── AD-Insurance_BTs.png ├── AD-Insurance_Flowmap.png ├── ad-analytics-simulator.yml ├── ad-betting.yml ├── ad-opentelemetry.yml ├── ad-php-test.yml ├── ad-simple.yml ├── api-gw.yml └── nodejs-flight-search.yml ├── infrastructure ├── dbmon │ ├── .gitignore │ ├── Dockerfile │ └── entrypoint.sh ├── machine │ ├── Dockerfile │ └── machineagent.sh ├── netviz │ └── Dockerfile ├── otelcollector │ ├── Dockerfile │ └── config.yaml └── phpproxy │ ├── .gitignore │ ├── Dockerfile │ └── entrypoint.sh ├── loaders ├── curl │ ├── Dockerfile │ └── loader.sh ├── example.json ├── phantomjs │ ├── Dockerfile │ ├── loader.js │ └── run.sh └── puppeteer │ ├── .dockerignore │ ├── Dockerfile │ ├── index.js │ ├── package.json │ └── run.sh ├── master ├── index.js └── package.json ├── nodes ├── appdynamics.json ├── backend.json ├── database.json ├── dotnetcore │ ├── .dockerignore │ ├── .gitignore │ ├── AppDynamicsConfig.json │ ├── Dockerfile │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── RequestLoggingMiddleware.cs │ ├── Startup.cs │ ├── WebException.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── dotnetcore.csproj │ └── entrypoint.sh ├── frontend.json ├── java │ ├── .gitignore │ ├── Dockerfile │ ├── entrypoint.sh │ ├── extras │ │ └── .gitkeep │ ├── javanode.iml │ ├── package.sh │ ├── pom.xml │ ├── run.sh │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── com │ │ │ │ └── appdynamics │ │ │ │ └── apmgame │ │ │ │ ├── HttpException.java │ │ │ │ └── JavaNode.java │ │ │ └── resources │ │ │ └── logback.xml │ └── target │ │ ├── classes │ │ └── logback.xml │ │ ├── dependency-jars │ │ ├── ehcache-2.10.5.jar │ │ ├── jakarta.servlet-api-5.0.0.jar │ │ ├── javax.json-1.1.jar │ │ ├── javax.json-api-1.1.jar │ │ ├── javax.servlet-api-3.1.0.jar │ │ ├── jetty-alpn-client-11.0.0.beta3.jar │ │ ├── jetty-client-11.0.0.beta3.jar │ │ ├── jetty-client-9.4.15.v20190215.jar │ │ ├── jetty-http-11.0.0.beta3.jar │ │ ├── jetty-http-9.4.15.v20190215.jar │ │ ├── jetty-io-11.0.0.beta3.jar │ │ ├── jetty-io-9.4.15.v20190215.jar │ │ ├── jetty-jakarta-servlet-api-5.0.1.jar │ │ ├── jetty-security-11.0.0.beta3.jar │ │ ├── jetty-security-9.4.15.v20190215.jar │ │ ├── jetty-server-11.0.0.beta3.jar │ │ ├── jetty-server-9.4.15.v20190215.jar │ │ ├── jetty-servlet-11.0.0.beta3.jar │ │ ├── jetty-servlet-9.4.15.v20190215.jar │ │ ├── jetty-util-11.0.0.beta3.jar │ │ ├── jetty-util-9.4.15.v20190215.jar │ │ ├── logback-classic-1.1.3.jar │ │ ├── logback-core-1.1.3.jar │ │ ├── mongo-java-driver-3.12.7.jar │ │ ├── mysql-connector-java-5.1.47.jar │ │ ├── mysql-connector-java-5.1.49.jar │ │ ├── protobuf-java-3.6.1.jar │ │ └── slf4j-api-1.7.25.jar │ │ └── javanode-1.0-SNAPSHOT.jar ├── mongo │ └── Dockerfile ├── mysql │ ├── Dockerfile │ ├── setup.php │ └── setup.sh ├── nodejs │ ├── .dockerignore │ ├── Dockerfile │ ├── index.js │ ├── node.sh │ ├── otel-tracing.js │ ├── package.json │ └── run.sh └── php │ ├── .gitignore │ ├── Dockerfile │ ├── agent-setup.php │ ├── appdynamics_agent.ini │ ├── entrypoint.sh │ ├── htaccess.txt │ ├── index.php │ ├── info.php │ ├── opcache.php │ └── run.sh ├── run.sh ├── scripts └── search.js └── tutorial ├── step-1.yml ├── step-2.yml ├── step-3.yml ├── step-4.yml ├── step-5.yml └── step-6.yml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard" 4 | ], 5 | "plugins": [ 6 | "standard", 7 | "promise", 8 | "json" 9 | ], 10 | "env": { 11 | "node": true 12 | }, 13 | "rules": { 14 | "space-before-function-paren": ["error", { 15 | "anonymous": "always", 16 | "named": "ignore", 17 | "asyncArrow": "ignore" 18 | }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | configs/*.yml 2 | node_modules/ 3 | package-lock.json 4 | nodes/*/node.log 5 | defaults.yml 6 | defaults-*.yml 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018 - 2024, Cisco Systems, Inc. and its affiliates 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APM Game 2 | 3 | Let the APM games begin! Have you ever had the evenings, where you wanted to play with your friends and families some rounds of identifying performance issues with [AppDynamics](https://www.appdynamics.com/), but missed the game to do that? -- Probably not, but here is **APM Games** anyhow, with some further use cases: 4 | 5 | - Build *interactive demos* where your audience can interact with AppDynamics themselves following your guidance. 6 | - Heavily inspired by demosim, you can build custom demos, if [DemoMonkey](https://github.com/Appdynamics/demomonkey/) does not fit all your needs. 7 | - Create custom screenshots for your presentations, articles, mails, ... 8 | - Reconstruct issues to have a reproducible environment. 9 | - Test configurations before bringing them into your customer's environment. 10 | - Show customers after a TDD, how their future implementation of AppDynamics might look like. 11 | 12 | # Features 13 | 14 | APM Game implements and uses the following features of AppDynamics: 15 | 16 | - APM 17 | - java 18 | - nodejs 19 | - php 20 | - .Net Core (dotnet) 21 | - User Experience 22 | - BRUM 23 | - Databases 24 | - mySQL 25 | - Analytics 26 | - Transaction 27 | - Log 28 | - Browser Records 29 | - Browser Sessions 30 | - Infrastructure 31 | - Server Visibility 32 | - Network Visibility 33 | 34 | # Installation 35 | 36 | Clone this project: 37 | 38 | ```shell 39 | git clone git@github.com:AppDynamics/apm-game.git 40 | ``` 41 | 42 | Install all prerequisites: 43 | - [Docker](https://www.docker.com/) 44 | - [Node.JS](https://nodejs.org/en/) 45 | 46 | Agent prerequisites (from download.appdynamics.com or the "Getting Started Wizard" of your controller): 47 | - PHP Agent 48 | - Java Agent 49 | - DB Agent 50 | - .NET Agent for Linux 51 | 52 | Copy agent files into the directories for the java and PHP node: 53 | 54 | ```shell 55 | cp /appdynamics-php-agent-x64-linux-.tar.bz2 nodes/php 56 | mv /appdynamics-php-agent-x64-linux-.tar.bz2 infrastructure/phpproxy 57 | mv /AppServerAgent-.zip nodes/java 58 | mv /db-agent-.zip infrastructure/dbmon 59 | mv /AppDynamics-DotNetCore-linux-x64-.zip nodes/dotnetcore 60 | ``` 61 | 62 | 63 | **Note:** The agent for nodejs is installed automatically. 64 | 65 | The docker images for the machine and network visibility agent are downloaded from docker store. You need to login to download these: 66 | 67 | ```shell 68 | docker login 69 | ``` 70 | 71 | Setup an [AppDynamics Platform](https://docs.appdynamics.com/display/latest/AppDynamics+Platform) or use your AppDynamics SaaS controller. 72 | 73 | # Usage 74 | 75 | 1. Configure your game using YAML. You can look into the file `config.yml` to get started. Read the **Configuration** section below to learn how you can describe your application environment. 76 | 77 | 2. Execute the `run.sh` 78 | 79 | 3. Wait for data in AppDynamics 80 | 81 | # Tutorial 82 | 83 | Before you go into the details of configuring your own environment, you can walk through this tutorial to get a quick understanding. Before you continue, follow the steps above to setup **APM Game** and your **AppDynamics Platform**. 84 | 85 | # Step 1 - Two java tiers 86 | 87 | Open the file `tutorial/step-1.yml` with your favourite editor and provide the credentials to your AppDynamics controller and events service. Next, execute 88 | `./run.sh tutorial/step-1.yml`. This script will first of all build all the docker files you need to run **APM Game**, next it will spin up some docker containers, check `docker ps` to see them coming up: 89 | 90 | ```shell 91 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 92 | e961843209b3 apm-game/machine "/usr/local/bin/mach…" 3 minutes ago Up 3 minutes step-1-machine-agent 93 | f0fb544fbb10 apm-game/java "/app/java.sh" 3 minutes ago Up 3 minutes 80/tcp step-1-frontend 94 | 7cb66fd11dee apm-game/puppeteer "dumb-init -- node i…" 3 minutes ago Up 3 minutes step-1-browser-0 95 | ffa7b6f2cc7d apm-game/netviz "./start.sh" 3 minutes ago Up 3 minutes step-1-netviz-agent 96 | 40a3487c47e6 apm-game/java "/app/java.sh" 3 minutes ago Up 3 minutes 80/tcp step-1-backend 97 | ``` 98 | 99 | As you can see, there are two java nodes, but also the machine agent, the network visibility agent and one container running "puppeteer" which is a headless version of Google Chrome. 100 | 101 | If you now look into the AppDynamics UI, you will see your Business Application with two nodes (frontend, backend) and one BT (/list). You should also see both tiers on the "Network Dashboard". And finally if you enable Analytics for your application, you should get transaction events. 102 | 103 | Compare what you see with the `step-1.yml` file: There are two java services (frontend, backend). The frontend has a `/list` http endpoint, that calls the `/list/items` endpoint on the backend and also calls a cache for 128ms. The endpoint `/list/items` just idles 1024ms before it returns a response. Below you have a single loader using `puppeteer` that continously calls the `/list` endpoint on the frontend. 104 | 105 | Next, add a few additional functionality to your configuration. First stop the `run.sh` by pressing `Ctrl+C`, next add the following changes to your file: 106 | 107 | - Add another endpoint `/get` to the frontend tier, that calls `/get/item` on the backend. Do not define `/get/item` on the backend tier! 108 | - Add `port: 3000` to the frontend tier as option 109 | - Add the new endpoint `http://frontend/get` to the list of urls for the loader. Also increase the count of loaders to 3. 110 | 111 | Now, execute `./run.sh tutorial/step-1.yml` again. You should see the following results: 112 | 113 | - A new BT `/get`, that has a lot of error transactions 114 | - You can open `http://localhost:3000/get` in your browser and add manual load to your frontend tier. 115 | - The load should increase by a factor of 3 116 | 117 | ## Step 2 - 3 Tiers and a backend 118 | 119 | Open `tutorial/step2.yml` and compare the content with `step1`. As you can see the setup is similar, but a few things are new: 120 | 121 | - A third tier *new-frontend* that is almost identical to the frontend, except that it has a different type (nodejs) 122 | - A fourth service called "storage", that runs without an agent. 123 | 124 | Spin up your environment by executing `./run.sh tutorial/step-2.yml`. Wait a few minutes and your flowmap should be updated: As expected, there is a new tier and a http remote service. 125 | 126 | Again, add some additional functionality to your configuration: 127 | 128 | - Add another tier *legacy-frontend* of type php that is identical to the other two frontends. 129 | - Add the option `aliases: [backup, www.appdynamics.com]` to the *storage* service. Next, add calls to `http://backup/item` and `http://www.appdynamics.com/item` to any of the endpoints. 130 | - Use [DemoMonkey](http://bit.ly/demomonkey) to change the type of the storage remote service to `fileserver` 131 | 132 | After restarting your environment, you should see an additional tier of type PHP and two more external services. Also your storage is now a fileserver! 133 | 134 | ## Step 3 - Errors & Randomness 135 | 136 | Open `tutorial/step3.yml` and compare the content with `step2`. Again, there are a few differences: 137 | 138 | - The backend has an `error` call with a probability of 10% 139 | - There is a *new-backend* and the *new-frontend* randomly either calls this or the old *backend*. 140 | 141 | When you spin up the environment you should see those two changes on your flowmap and also in the snapshots. 142 | 143 | Now, you are able to create performance issues with **APM Game**. But, again, we can add a few more things: 144 | 145 | - Edit *storage* and add the following after the `sleep,500`: 146 | ```YAML 147 | - call: sleep,8000 148 | schedule: "* */2 * * * * *" 149 | ``` 150 | - Edit *backend* and replace `- http://storage/item` with: 151 | ```YAML 152 | - call: http://storage/item 153 | remoteTimeout: 2000 154 | ``` 155 | 156 | After an environment restart you should see error transactions for the communication between *backend* and *storage*. The error description should be a timeout on the *backend* 157 | 158 | 159 | ## Step 4 - EUM 160 | 161 | Again, compare `step-3.yml` with `step-4.yml`. This time, there is only one difference: At the top is a section for EUM configuration. Add the missing EUM key and start the environment. After a few minutes you should see some data in EUM. 162 | 163 | Since all your issues are in the backend, the data in EUM is not very interesting, yet. To change this, add the following service to `step-4.yml`: 164 | 165 | ```YAML 166 | cdn: 167 | type: nodejs 168 | agent: no 169 | endpoints: 170 | http: 171 | /logo.png: 172 | - sleep,1000 173 | /script.js: 174 | - sleep,2000 175 | ``` 176 | 177 | Also, add `image,http://cdn/logo.png` to the `/list` of *frontend*. When you now restart your environment, EUM will report issues with the picture. In the same way you can use `script,http://cdn/script.js`. Finally you can use `ajax,http://backend/list/items` to add an ajax call to the backend. 178 | 179 | ## Step 5 - Analytics 180 | 181 | This time we start with a completely new environment. Have a look into `step-5.yml`. As you can see, it is rather simple, but there are a few new things: 182 | 183 | - Custom data is added to snapshots & analytics using the `data`. This command comes with a lot of flexibility, so see how the different examples behave. Note, that `chance` can only be used with nodejs! 184 | - Besides `data` there are some `log` statements, that write some entries into a application log file. You can configure Log Analytics to pick up those log files from `/logs///node.log`. 185 | - The loaders section has multiple steps, that look like a journey. After running the environment for a view minutes, check the EUM sessions screen, you will see that every loop is recorded as a separate session, following those steps! 186 | 187 | Play around with the data collectors. Here are a few things you can try it: 188 | 189 | - Go to the [chance.js homepage](https://chancejs.com/) and try out a few of the possibilities using the nodejs frontend. 190 | - Add some additional `log` statements using different severity levels (info, debug, error, warn, trace) 191 | - Add an additional http endpoint to the frontend (e.g. /orderConfirmation) and add it to the loaders' loop 192 | 193 | With those capabilities you are already able to build some good environments with business data. Of course, you might want to have some relationship in your data, for example certain products have a certain price or a certain credit card type is processed very slow. To anticipate all this possibilities, you can load a custom script from `scripts/` using the `script` command. Review the `sample.js` and try to come up with your own example. 194 | 195 | ## Step 6 - Run with Dynamic Attach 196 | 197 | With `step-6.yml` we go back to the simple environment we had at the beginning: a frontend and a backend java application. The difference is, that we start the environment without machine agent and the application agents are also disabled, because we want to use [Dynamic Attach](https://github.com/Appdynamics/Dynamic-Agent-MA) to instrument the applications after startup. Just run `./run.sh tutorial/step-6.yml` and wait until the load is generated. Next, clone the [Dynamic-Agent-MA](https://github.com/Appdynamics/Dynamic-Agent-MA) repository, edit the controller.env to contain your credentials and add the following two lines to the end: 198 | 199 | ``` 200 | TIER_NAME_FROM=CONTAINER_LABEL 201 | TIER_NAME_FROM_VALUE=service-name 202 | ``` 203 | 204 | Execute the `run.sh` and wait a few minutes, then check with your AppDynamics Controller, that the agents have been attached and that you can see your application being instrumented. 205 | 206 | 207 | ## Step 7 - Extend & Contribute 208 | 209 | After you finished step 5, you should be able to leverage all the capabilities provided by APM Game. At same point, you will find some bugs or miss some features. In this case, open an issue at https://github.com/Appdynamics/apm-game/issues. If you are able to fix the bug yourself or implement the feature, please do so and provide a pull request. To get you started, follow these instructions to add some simple `hello world` features: 210 | 211 | - Open the file `nodes/nodejs/index.js`: this is the implementation of the nodejs agent. 212 | - Look for a line, that says ```resolve(`${call} is not supported`)```: above you see all the commands, that are available for nodejs. 213 | - Add the following `else if` block above the last `else`: 214 | 215 | ```javascript 216 | ... 217 | else if(call.startsWith('hello')) { 218 | resolve('hello world') 219 | } 220 | ... 221 | ``` 222 | 223 | - Add a `- hello` command to one of your endpoints. Use a node that has a published port 224 | - Run your environment 225 | - Call the endpoint using your browser on the configured port. The output should contain a `hello world` 226 | 227 | Since you don't want to spin up all the containers to test a feature, there is a `run.sh` for every node type. Review them to learn how you can setup your development environment. 228 | 229 | 230 | # Configuration 231 | 232 | Configurations for **APM Game** are given in YAML files. By default the `run.sh` looks for a file called config.yml, but you can provide another file as parameter: 233 | 234 | ```shell 235 | ./run.sh configs/myconfig.yml 236 | ``` 237 | 238 | The configuration has 5 top-level sections: **global**, **apm**, **services**, **loaders** and **chaos**: 239 | 240 | ```YAML 241 | global: 242 | ... 243 | apm: 244 | ... 245 | services: 246 | ... 247 | loaders: 248 | ... 249 | chaos: 250 | ... 251 | ``` 252 | 253 | ## Global 254 | 255 | The **global** section is optional. It can be used to enable or disable certain features of the simulation: 256 | 257 | - **machine**: Run with AppDynamics machine agent (default: true). 258 | - **netviz**: Run with AppDynamics network visibility agent (default: true). 259 | - **dbmon**: Run with AppDynamics database monitoring agent (default: 'maybe' -- it is only run if a database node is available). 260 | - **loaders**: Run with containers that generate load (default: true). 261 | - **services**: Run with containers that provide services (default: true). 262 | - **chaos**: Run with containers that generate chaos (default: true). 263 | 264 | Most of the time you don't need to change any of these default settings. Use them, if you for example have an independent machine agent running or if you need to test certain functionalities without having to run all other components. 265 | 266 | ## APM 267 | 268 | In the **apm** section you can provide all properties required to configure the AppDynamics agents. For the APM agents you need to configure the following: 269 | 270 | - **controller**: The URL of your controller, e.g. `https://controller.example.com:8090` 271 | - **accountName**: Your short account name, e.g. `customer1` 272 | - **accountAccessKey**: The access key of your account, eg. `ffffffff-ffff-ffff-ffff-fffffffffff` 273 | - **applicationName**: The name of the business application used by all of your agents, e.g. `apm_game` 274 | 275 | If you want to use analytics capabilities, set the following: 276 | 277 | - eventsService: The URL of your analytics endpoints, e.g. `http://analytics.example.com:9080` 278 | - globalAccountName: Your global/long account name, e.g. `customer1_ffffffff-ffff-ffff-ffff-ffffffffffff` 279 | 280 | Finally, you also can setup End User Monitoring in an `eum` sub-section: 281 | 282 | - **appKey**: The key of your EUM app, e.g. `AD-FFF-FFF-FFF` 283 | - **adrumExtUrlHttp**: URL to load the adrum-ext.js from via http, e.g. `http://cdn.appdynamics.com` 284 | - **adrumExtUrlHttps**: URL to load the adrum-ext.js from via https, e.g. `https://cdn.appdynamics.com` 285 | - **beaconUrlHttp**: URL for the beacons via http, e.g. `http://col.eum-appdynamics.com` 286 | - **beaconUrlHttps**: URL for the beacons via https, e.g.`https://col.eum-appdynamics.com` 287 | 288 | A final **apm** configuration looks like following: 289 | 290 | ```YAML 291 | apm: 292 | controller: https://controller.example.com:8090 293 | accountName: customer1 294 | accountAccessKey: ffffffff-ffff-ffff-ffff-fffffffffff 295 | applicationName: apm_game 296 | eventsService: http://analytics.example.com:9080 297 | globalAccountName: customer1_ffffffff-ffff-ffff-ffff-ffffffffffff 298 | eum: 299 | appKey: 'AD-FFF-FFF-FFF' 300 | adrumExtUrlHttp: 'http://cdn.appdynamics.com' 301 | adrumExtUrlHttps: 'https://cdn.appdynamics.com' 302 | beaconUrlHttp: 'http://col.eum-appdynamics.com' 303 | beaconUrlHttps: 'https://col.eum-appdynamics.com' 304 | ``` 305 | 306 | ## services 307 | 308 | In this section you provide all the tiers/nodes and remote services that are contained in your business application. Each sub-section is the name of a service. This name will be used to name the docker image as well as the tier within AppDynamics. Since the services use these names also to communicate with each other the name should be a valid hostname, e.g. `frontend`, `backend-v2` or `payment-provider-1`, ... 309 | 310 | A service can have the following properties: 311 | 312 | - **type** (required): Define the type of this service. You can currently use the following: `java`, `nodejs`, `php`, `dotnet` `mysql` and `custom`. **Hint**: Prefer nodejs for agentless services and also if you want to build a big environment, since it comes with the lowest overhead. 313 | - **agent**: Set to `no` or `yes` to disable or enable the appdynamics agent. 314 | - **count**: Set the number of instances, that will be started for this service. 315 | - **port**: Set a port which will be exposed to your docker host. So if you run locally, you can access this service via `http://localhost:` 316 | - **endpoints** (java, nodejs, php only): Define multiple endpoints for this service. Read below to learn how to define endpoints. 317 | - **aliases**: Provide a list of network name aliases. This is useful for agentless services, that serve as multiple remote services, e.g. multiple payment providers. **Hint**: You can use any name for an alias, even some existing domain names (e.g. www.appdynamics.com)! 318 | - **labels**: You can provide a list of docker labels, that will be visible in the "container" view. 319 | - **options** (nodejs only): For nodejs you can set options `connectionDelay` and `lossRate`. `connectionDelay` will force the webserver to wait the given number of milliseconds before it accepts a connection. `lossRate` is a number between 0 and 1 that defines the probability, if a request is terminated early. 320 | - **disabled**: Set this to `yes` to temporarily disable the service without removing it from the configuration file. 321 | - **databases** (mysql only): Define multiple databases, that are created on startup on this database service. Read below to learn how to define databases and tables. 322 | - **image** (custom only): If you set the `type` to custom, you can define any docker image to be used for this service. 323 | 324 | Without endpoints and databases a configuration might look like the following: 325 | 326 | ```YAML 327 | services: 328 | frontend: 329 | type: nodejs 330 | labels: 331 | version: v1.0 332 | dc: FRA 333 | agent: yes 334 | port: 3000 335 | options: 336 | connectionDelay: 500 337 | endpoints: 338 | ... 339 | backend: 340 | type: java 341 | agent: yes 342 | endpoints: 343 | ... 344 | ext-payment: 345 | type: nodejs 346 | agent: no 347 | aliases: [ext-payment-1, ext-payment-2] 348 | endpoints: 349 | ... 350 | backend-db: 351 | type: mysql 352 | databases: 353 | ... 354 | ``` 355 | 356 | ### Endpoints 357 | 358 | Services with type nodejs, php or java can serve multiple endpoints via different protocol (right now only http... ). Below each protocol you can list the names of the endpoints with a sequence of calls: 359 | 360 | ``` 361 | ... 362 | frontend: 363 | ... 364 | endpoints: 365 | http: 366 | /login: 367 | ... 368 | /addtocart: 369 | ... 370 | /checkout: 371 | ... 372 | ``` 373 | 374 | The *call sequences* below each endpoint are the simulated logic of your business application. Since the order of elements matters, you provide them in YAML list notation: 375 | 376 | ```YAML 377 | ... 378 | /checkout: 379 | - http://backend/cart/checkout 380 | - sleep,200 381 | - call: error,500,Aborted 382 | probability: 0.1 383 | schedule: "* */2 * * * * *" 384 | - call: data 385 | id: price 386 | type: int 387 | value: [32,16,8] 388 | - ... 389 | ... 390 | ``` 391 | 392 | The example above first executes a call to another service, called backend, then sleeps for 200 milliseconds and afterwards an error is thrown with a probability of 10%. Here is a list of supported commands and modifiers: 393 | 394 | - **Commands** are like lines of code, that are executed by a service. You can call as many of them as you like to define an endpoint. 395 | - `http:///`: Call another service via http. 396 | - `sql:///?query=` (php & java): Call a database service via SQL. 397 | - `sleep,`: Stop processing for `` milliseconds. Note, that the call graph will contain a language-specific `sleep` method, so use it especially with agent-less services and prefer `slow` for those having an agent. 398 | - `slow,`: Slow down processing by around `` milliseconds. The timeout is not accurate, so it will most of the time longer than the value given in ``. 399 | - `cache,`: Call a remote service of type cache. For Java this is ehcache2, for PHP and nodejs there is no real cache implementation, but they will tell you that a redis service was called. 400 | - `error,,`: Throw an error with HTTP code `` and message ``. 401 | - `log,,` (java & nodejs): Write a log message. The messages are stored on a shared volume, accessible at `/logs///node.log` 402 | - `image,`: Put an `>` on the result page. This can be used to slow down end user responses. 403 | - `script,`: Put an `"; 261 | } 262 | if (call.startsWith("ajax")) { 263 | String src = call.split(",")[1]; 264 | return ""; 265 | } 266 | return ":" + call + " is not supported"; 267 | } 268 | 269 | protected String processIntData(String id, long value) { 270 | return metricAndEventReporter.addSnapshotData(id, value, allScopes) ? id + " data added" : id + " data not added"; 271 | } 272 | 273 | protected String processDoubleData(String id, double value) { 274 | return metricAndEventReporter.addSnapshotData(id, value, allScopes) ? id + " data added" : id + " data not added"; 275 | } 276 | 277 | protected String processStringData(String id, String value) { 278 | return metricAndEventReporter.addSnapshotData(id, value, allScopes) ? id + " data added" : id + " data not added"; 279 | } 280 | 281 | protected String processData(JsonObject data) { 282 | if(!hasMetricAndEventReporter()) { 283 | return "Data not processed: no agent installed."; 284 | } 285 | 286 | if(!data.containsKey("id")) { 287 | return "Data not processed: No id provided"; 288 | } 289 | 290 | if(!data.containsKey("value")) { 291 | return "Data not processed: No value provided"; 292 | } 293 | 294 | String id = data.getString("id"); 295 | String type = data.containsKey("type") ? data.getString("type").toLowerCase() : "string"; 296 | 297 | JsonValue value = data.get("value"); 298 | 299 | if (value.getValueType() == JsonValue.ValueType.ARRAY) { 300 | JsonArray arr = (JsonArray) value; 301 | int index = ThreadLocalRandom.current().nextInt(arr.size()); 302 | value = arr.get(index); 303 | } 304 | 305 | switch(type) { 306 | case "int": 307 | return processIntData(id, ((JsonNumber)value).longValue()); 308 | case "double": 309 | return processDoubleData(id, ((JsonNumber)value).doubleValue()); 310 | default: 311 | return processStringData(id, ((JsonString)value).getString()); 312 | } 313 | } 314 | 315 | protected String preProcessCall(JsonValue call) throws IOException { 316 | 317 | boolean catchExceptions = true; 318 | int remoteTimeout = Integer.MAX_VALUE; 319 | 320 | if (call.getValueType() == JsonValue.ValueType.ARRAY) { 321 | JsonArray arr = (JsonArray) call; 322 | int index = ThreadLocalRandom.current().nextInt(arr.size()); 323 | call = arr.get(index); 324 | } 325 | if (call.getValueType() == JsonValue.ValueType.OBJECT) { 326 | JsonObject obj = (JsonObject) call; 327 | call = obj.getJsonString("call"); 328 | 329 | if(((JsonString) call).getString().startsWith("data")) { 330 | return this.processData(obj); 331 | } 332 | 333 | if(obj.containsKey("probability")) { 334 | double probability = obj.getJsonNumber("probability").doubleValue(); 335 | if (probability * 100 < ThreadLocalRandom.current().nextInt(100)) { 336 | return call + " was not probable"; 337 | } 338 | } 339 | if(obj.containsKey("catchExceptions")) { 340 | catchExceptions = obj.getBoolean("catchExceptions"); 341 | } 342 | if(obj.containsKey("remoteTimeout")) { 343 | remoteTimeout = obj.getInt("remoteTimeout"); 344 | } 345 | } 346 | return this.processCall(((JsonString) call).getString(), catchExceptions, remoteTimeout); 347 | } 348 | 349 | public void handleEndpoint(HttpServletResponse response, JsonArray endpoint, boolean withEum) throws IOException { 350 | response.setStatus(HttpServletResponse.SC_OK); 351 | 352 | StringBuilder result = new StringBuilder(); 353 | 354 | for (JsonValue entry : endpoint) { 355 | result.append(this.preProcessCall(entry)); 356 | } 357 | 358 | if(withEum) { 359 | response.getWriter().println("" + NodeServlet.config.getString("name") + "" + result); 360 | } else { 361 | response.getWriter().println(result); 362 | } 363 | } 364 | 365 | @Override 366 | protected void doGet(HttpServletRequest request, 367 | HttpServletResponse response) throws ServletException, 368 | IOException { 369 | String endpoint = request.getRequestURI(); 370 | logger.info("Endpoint: {}", endpoint); 371 | 372 | boolean withEum = NodeServlet.apmConfig.containsKey("eum"); 373 | 374 | String contentType = request.getContentType(); 375 | 376 | if("application/json".equals(contentType) || "javascript".equals(request.getParameter("output"))) { 377 | response.setContentType(contentType); 378 | withEum = false; 379 | } else if (contentType == null) { 380 | response.setContentType("text/html;charset=utf-8"); 381 | } 382 | 383 | if(request.getParameter("uniqueSessionId") != null) { 384 | metricAndEventReporter.addSnapshotData("uniqueSessionId", request.getParameter("uniqueSessionId"), allScopes); 385 | } 386 | 387 | 388 | try { 389 | if (NodeServlet.endpoints.containsKey(endpoint)) { 390 | this.handleEndpoint(response, NodeServlet.endpoints.getJsonArray(endpoint), withEum); 391 | } else if (NodeServlet.endpoints.containsKey(endpoint.substring(1))) { 392 | this.handleEndpoint(response, NodeServlet.endpoints.getJsonArray(endpoint.substring(1)), withEum); 393 | } else { 394 | response.setStatus(HttpServletResponse.SC_NOT_FOUND); 395 | response.getWriter().println(404); 396 | } 397 | } catch (HttpException e) { 398 | response.setStatus(e.getCode()); 399 | response.getWriter().println(e.getMessage()); 400 | } catch (IOException e) { 401 | response.setStatus(500); 402 | response.getWriter().println(e.getMessage()); 403 | } 404 | } 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /nodes/java/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %date{ISO8601} [%thread] [%X{AD.requestGUID}] %-5level %logger - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | ${LOG_DIRECTORY:-.}/node.log 14 | 15 | 16 | %date{ISO8601} [%thread] [%X{AD.requestGUID}] %-5level %logger - %msg%n 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /nodes/java/target/classes/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %date{ISO8601} [%thread] [%X{AD.requestGUID}] %-5level %logger - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | ${LOG_DIRECTORY:-.}/node.log 14 | 15 | 16 | %date{ISO8601} [%thread] [%X{AD.requestGUID}] %-5level %logger - %msg%n 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/ehcache-2.10.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/ehcache-2.10.5.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jakarta.servlet-api-5.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jakarta.servlet-api-5.0.0.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/javax.json-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/javax.json-1.1.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/javax.json-api-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/javax.json-api-1.1.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/javax.servlet-api-3.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/javax.servlet-api-3.1.0.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-alpn-client-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-alpn-client-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-client-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-client-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-client-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-client-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-http-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-http-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-http-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-http-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-io-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-io-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-io-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-io-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-jakarta-servlet-api-5.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-jakarta-servlet-api-5.0.1.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-security-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-security-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-security-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-security-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-server-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-server-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-server-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-server-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-servlet-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-servlet-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-servlet-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-servlet-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-util-11.0.0.beta3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-util-11.0.0.beta3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/jetty-util-9.4.15.v20190215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/jetty-util-9.4.15.v20190215.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/logback-classic-1.1.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/logback-classic-1.1.3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/logback-core-1.1.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/logback-core-1.1.3.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/mongo-java-driver-3.12.7.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/mongo-java-driver-3.12.7.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/mysql-connector-java-5.1.47.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/mysql-connector-java-5.1.47.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/mysql-connector-java-5.1.49.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/mysql-connector-java-5.1.49.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/protobuf-java-3.6.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/protobuf-java-3.6.1.jar -------------------------------------------------------------------------------- /nodes/java/target/dependency-jars/slf4j-api-1.7.25.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/dependency-jars/slf4j-api-1.7.25.jar -------------------------------------------------------------------------------- /nodes/java/target/javanode-1.0-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appdynamics/apm-game/c0babe25500782a7d27cc8b22c19fcfcb75fd971/nodes/java/target/javanode-1.0-SNAPSHOT.jar -------------------------------------------------------------------------------- /nodes/mongo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo:latest 2 | -------------------------------------------------------------------------------- /nodes/mysql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:5.7 2 | RUN apt-get update && apt-get install -y --no-install-recommends php-cli && rm -rf /var/lib/apt/lists/* 3 | COPY setup.php /tmp/ 4 | COPY setup.sh /docker-entrypoint-initdb.d/ 5 | -------------------------------------------------------------------------------- /nodes/mysql/setup.php: -------------------------------------------------------------------------------- 1 | databases as $database => $tables) { 9 | $result .= "CREATE DATABASE ".$database.";".PHP_EOL; 10 | $result .= "USE ".$database.";".PHP_EOL; 11 | foreach($tables as $table => $columns) { 12 | $result .="CREATE TABLE ".$table." (".PHP_EOL; 13 | foreach($columns as $column) { 14 | if($column === 'id') { 15 | $result .= " ".$column." INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,".PHP_EOL; 16 | } else { 17 | $result .= " ".$column. " VARCHAR(255),".PHP_EOL; 18 | } 19 | } 20 | 21 | $result = substr($result,0,-2).PHP_EOL; 22 | 23 | $result .=') ENGINE=InnoDB;'.PHP_EOL; 24 | } 25 | } 26 | 27 | echo '=====.PHP_EOL'; 28 | 29 | echo $result; 30 | 31 | echo '=====.PHP_EOL'; 32 | 33 | file_put_contents("/tmp/create.sql",$result); 34 | -------------------------------------------------------------------------------- /nodes/mysql/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # APP_CONFIG={"type":"mysql","databases":{"shop":{"carts":["id","name","value"],"customers":["id","name","email"]}},"name":"backend-db"} 3 | php /tmp/setup.php 4 | mysql -uroot -p${MYSQL_ROOT_PASSWORD} < /tmp/create.sql 5 | -------------------------------------------------------------------------------- /nodes/nodejs/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | run.sh 5 | -------------------------------------------------------------------------------- /nodes/nodejs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | RUN chmod +x /app/node.sh 7 | EXPOSE 80 8 | CMD ["/app/node.sh"] 9 | -------------------------------------------------------------------------------- /nodes/nodejs/index.js: -------------------------------------------------------------------------------- 1 | const process = require('process') 2 | const url = require('url') 3 | const path = require('path') 4 | const chance = require('chance').Chance() 5 | 6 | const config = JSON.parse(process.env.APP_CONFIG) 7 | const apm = JSON.parse(process.env.APM_CONFIG) 8 | 9 | const customCodeDir = process.env.CUSTOM_CODE_DIR 10 | 11 | const logDir = process.env.LOG_DIRECTORY ? process.env.LOG_DIRECTORY : '.' 12 | 13 | const controller = url.parse(apm.controller) 14 | 15 | var appdynamics = { 16 | parseCorrelationInfo: function () { 17 | return { 18 | headers: {} 19 | } 20 | }, 21 | getTransaction: function () { 22 | return { 23 | startExitCall: function () {}, 24 | endExitCall: function () {}, 25 | addSnapshotData: function () {}, 26 | addAnalyticsData: function () {} 27 | } 28 | } 29 | } 30 | 31 | var withEum = false 32 | 33 | if (config.agent === 'yes') { 34 | appdynamics = require('appdynamics') 35 | 36 | var appdynamicsProfile = { 37 | controllerHostName: controller.hostname, 38 | controllerPort: controller.port, 39 | controllerSslEnabled: controller.protocol.startsWith('https'), 40 | accountName: apm.accountName, 41 | accountAccessKey: apm.accountAccessKey, 42 | applicationName: apm.applicationName, 43 | tierName: config.name, 44 | nodeName: `${config.name}-${config.nodeid}`, 45 | libagent: true, 46 | debug: true 47 | } 48 | 49 | if (apm.eventsService && apm.globalAccountName) { 50 | appdynamicsProfile.analytics = { 51 | host: 'machine-agent', 52 | port: 9090, 53 | SSL: false 54 | } 55 | } 56 | 57 | if (typeof apm.eum === 'object') { 58 | withEum = true 59 | var eumConfig = Object.assign({ 60 | xd: { 61 | enable: false 62 | } 63 | }, apm.eum) 64 | } 65 | console.log('Appdynamics Profile', appdynamicsProfile) 66 | appdynamics.profile(appdynamicsProfile) 67 | } else if (config.agent === 'otel') { 68 | require('./otel-tracing.js') 69 | } 70 | 71 | const express = require('express') 72 | const morgan = require('morgan') 73 | const log4js = require('log4js') 74 | const http = require('http') 75 | const cronmatch = require('cronmatch') 76 | var bodyParser = require('body-parser') 77 | const sleep = require('sleep') 78 | const rp = require('request-promise') 79 | 80 | log4js.configure({ 81 | appenders: { 82 | FILE: { 83 | type: 'file', 84 | filename: `${logDir}/node.log`, 85 | layout: { 86 | type: 'pattern', 87 | pattern: '%d{yyyy-MM-dd hh:mm:ss,SSS} [%z] [%X{AD.requestGUID}] %p %c - %m' 88 | } 89 | }, 90 | CONSOLE: { 91 | type: 'stdout', 92 | layout: { 93 | type: 'pattern', 94 | pattern: '%d{yyyy-MM-dd hh:mm:ss,SSS} [%z] [%X{AD.requestGUID}] %p %c - %m' 95 | } 96 | } 97 | }, 98 | categories: { default: { appenders: ['CONSOLE', 'FILE'], level: 'info' } } 99 | }) 100 | 101 | var logger = log4js.getLogger() 102 | logger.level = 'debug' 103 | 104 | const app = express() 105 | 106 | app.use(bodyParser.json()) // for parsing application/json 107 | app.use(bodyParser.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded 108 | app.use(morgan(':remote-addr - ":method :url HTTP/:http-version" :status :res[content-length] ":user-agent" - :response-time ms', { 109 | stream: { 110 | write: function (str) { 111 | logger.debug(str.trim('\n')) 112 | } 113 | } 114 | })) 115 | 116 | var port = parseInt(process.argv[2]) 117 | 118 | const endpoints = config.endpoints.http 119 | 120 | const useRp = config.hasOwnProperty('options') && config.options.hasOwnProperty('httpLibrary') && config.options.httpLibrary == 'request-promise' 121 | 122 | Object.keys(endpoints).forEach(function (key) { 123 | if (!key.startsWith('/')) { 124 | endpoints['/' + key] = endpoints[key] 125 | delete endpoints[key] 126 | } 127 | }) 128 | 129 | if (isNaN(port)) { 130 | port = 3000 131 | } 132 | 133 | 134 | function buildResponse(timeout) { 135 | const start = process.hrtime() 136 | var elapsed = process.hrtime(start) 137 | var response = '' 138 | while (elapsed[0] * 1000000000 + elapsed[1] < timeout * 1000000) { 139 | response += ' ' 140 | elapsed = process.hrtime(start) 141 | } 142 | return response.length + ' slow response' 143 | } 144 | 145 | function loadFromCache(timeout, txn) { 146 | const start = process.hrtime() 147 | var elapsed = process.hrtime(start) 148 | var response = '' 149 | while(elapsed[0] * 1000000000 + elapsed[1] < timeout * 1000000) { 150 | var exit = txn.startExitCall({ 151 | exitType: 'EXIT_CACHE', 152 | label: 'Redis Cache', 153 | backendName: 'Redis', 154 | identifyingProperties: { 155 | 'SERVER POOL': 'redis:6380' 156 | } 157 | }); 158 | 159 | elapsed = process.hrtime(start) 160 | response += ' ' 161 | 162 | txn.endExitCall(exit) 163 | } 164 | return response.length + ' send data to cache' 165 | } 166 | 167 | function processData (resolve, reject, req, data) { 168 | if (!data.id) { 169 | reject('Data not processed: No id provided') 170 | } 171 | 172 | if (data.chance) { 173 | var fna = data.chance.split(',') 174 | var fn = fna.shift() 175 | var attributes = fna.reduce((c, a) => { var [k, v] = a.split(':'); c[k] = isNaN(parseInt(v)) ? v : parseInt(v); return c }, {}) 176 | data.value = chance[fn](attributes) 177 | } 178 | 179 | if (!data.value) { 180 | reject('Data not processed: No value provided') 181 | } 182 | 183 | var value = data.value 184 | var id = data.id 185 | 186 | if (Array.isArray(value)) { 187 | value = value[Math.floor(Math.random() * value.length)] 188 | } 189 | 190 | var txn = appdynamics.getTransaction(req) 191 | if (txn) { 192 | txn.addSnapshotData(id, value) 193 | txn.addAnalyticsData(id, value) 194 | resolve(`${id} data added: ${value}`) 195 | } 196 | 197 | reject('No data added: Transaction not found.') 198 | } 199 | 200 | function logMessage(level, message) { 201 | if (['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(level)) { 202 | logger[level](message) 203 | } else { 204 | logger.info(message) 205 | } 206 | return 'Logged (' + level + '): ' + message 207 | } 208 | 209 | function executeCustomScript(script, req, resolve, reject) { 210 | var txn = appdynamics.getTransaction(req) 211 | var r = require(path.join(customCodeDir, script))({ 212 | logger: logger, 213 | req: req, 214 | cronmatch: cronmatch, 215 | txn: txn, 216 | sleep: sleep.msleep, 217 | add: (id, value) => { 218 | txn.addAnalyticsData(id, value) 219 | txn.addSnapshotData(id, value) 220 | }, 221 | chance: chance 222 | }) 223 | if (r === false) { 224 | reject(`Script ${script} was not executed successfully`) 225 | } else if (typeof r === "object" && r.hasOwnProperty('code') && r.hasOwnProperty('code')) { 226 | reject({ code: r.code, message: r.message }) 227 | } else if (typeof r === 'string') { 228 | resolve(r) 229 | } else { 230 | resolve(`Script ${script} was executed successfully`) 231 | } 232 | } 233 | 234 | function callRemoteService(call, useRp, catchExceptions, remoteTimeout, req, resolve, reject) { 235 | if (useRp) { 236 | rp.get({ 237 | uri: url.parse(call), 238 | json: true, 239 | timeout: remoteTimeout 240 | }).then(function (body) { 241 | resolve(body) 242 | }).catch(function (err) { 243 | if (catchExceptions) { 244 | resolve(err) 245 | } else { 246 | reject(err) 247 | } 248 | }) 249 | } else { 250 | var headers = { 251 | 'Content-Type': 'application/json' 252 | } 253 | // Turn the node into a proxy / api gateway if there is no agent installed 254 | if (config.agent !== 'yes') { 255 | headers = req.headers 256 | } 257 | var opts = Object.assign(url.parse(call), { 258 | headers: headers 259 | }) 260 | const r = http.get(opts, function (res, req) { 261 | const body = [] 262 | res.on('data', (chunk) => body.push(chunk)) 263 | res.on('end', () => resolve(body.join(''))) 264 | }).on('error', function (err) { 265 | if (catchExceptions) { 266 | resolve(err) 267 | } else { 268 | reject(err) 269 | } 270 | }) 271 | r.setTimeout(remoteTimeout, function () { 272 | reject({ code: 500, message: 'Read timed out' }) 273 | }) 274 | } 275 | } 276 | 277 | function processCall(call, req) { 278 | return new Promise(function (resolve, reject) { 279 | console.log(call) 280 | var remoteTimeout = 1073741824 281 | 282 | var catchExceptions = true 283 | 284 | // If call is an array, select one element as call 285 | if (Array.isArray(call)) { 286 | call = call[Math.floor(Math.random() * call.length)] 287 | } 288 | // If call is an object, check for probability 289 | if (typeof call === 'object') { 290 | if (call.hasOwnProperty('probability') && call.probability <= Math.random()) { 291 | resolve(`${call.call} was not probable`) 292 | return 293 | } 294 | if (call.hasOwnProperty('schedule') && !cronmatch.match(call.schedule, new Date())) { 295 | resolve(`${call.call} was not scheduled`) 296 | return 297 | } 298 | if (call.hasOwnProperty('remoteTimeout')) { 299 | remoteTimeout = call.remoteTimeout 300 | } 301 | if (call.hasOwnProperty('catchExceptions')) { 302 | catchExceptions = call.catchExceptions 303 | } 304 | if (call.hasOwnProperty('call') && call.call === 'data') { 305 | return processData(resolve, reject, req, call) 306 | } 307 | call = call.call 308 | } 309 | if (call.startsWith('error')) { 310 | const [_, code, message] = call.split(',') 311 | reject({ code, message }) 312 | } else if (call.startsWith('sleep')) { 313 | const [_, timeout] = call.split(',') 314 | setTimeout(function () { 315 | resolve(`Slept for ${timeout}`) 316 | }, timeout) 317 | } else if (call.startsWith('slow')) { 318 | const [_, timeout] = call.split(',') 319 | resolve(buildResponse(timeout)) 320 | } else if (call.startsWith('http://')) { 321 | callRemoteService(call, useRp, catchExceptions, remoteTimeout, req, resolve, reject) 322 | } else if (call.startsWith('image')) { 323 | const [_, src] = call.split(',') 324 | resolve(``) 325 | } else if (call.startsWith('script')) { 326 | const [_, src] = call.split(',') 327 | resolve(``) 328 | } else if (call.startsWith('ajax')) { 329 | const [_, src] = call.split(',') 330 | resolve(``) 331 | } else if (call.startsWith('cache')) { 332 | const [_, timeout] = call.split(',') 333 | const txn = appdynamics.getTransaction(req) 334 | resolve(loadFromCache(timeout, txn)) 335 | } else if (call.startsWith('log')) { 336 | const logging = call.split(',') 337 | if (logging.length > 2) { 338 | resolve(logMessage(logging[1], logging[2])) 339 | } else { 340 | resolve(logMessage('info', logging[1])) 341 | } 342 | } else if (call.startsWith('code')) { 343 | const [_, script] = call.split(',') 344 | executeCustomScript(script, req, resolve, reject) 345 | } else { 346 | // No other methods are currently implemented 347 | resolve(`${call} is not supported`) 348 | } 349 | }) 350 | } 351 | 352 | async function processRequest(req, res, params) { 353 | const path = url.parse(req.url).pathname 354 | 355 | var txn = appdynamics.getTransaction(req) 356 | 357 | if (txn) { 358 | var signularityHeader = appdynamics.parseCorrelationInfo(req).headers.singularityheader 359 | 360 | if (typeof signularityHeader !== 'undefined') { 361 | const sh = new url.URLSearchParams(signularityHeader.replace(/\*/g, '&')) 362 | logger.addContext('AD.requestGUID', 'AD_REQUEST_GUID[' + sh.get('guid') + ']') 363 | } 364 | 365 | if (params.unique_session_id) { 366 | txn.addSnapshotData('uniqueSessionId', req.query.unique_session_id) 367 | txn.addAnalyticsData('uniqueSessionId', req.query.unique_session_id) 368 | } 369 | 370 | if (params.hasOwnProperty('analytics') && typeof params.analytics === 'object') { 371 | Object.keys(params.analytics).forEach(function (key) { 372 | logger.debug('Adding analytics data: ', key, params.analytics[key]) 373 | txn.addAnalyticsData(key, params.analytics[key]) 374 | txn.addSnapshotData(key, params.analytics[key]) 375 | }) 376 | } 377 | } 378 | 379 | if (endpoints.hasOwnProperty(path)) { 380 | try { 381 | const results = [] 382 | for (let i = 0; i < endpoints[path].length; i++) { 383 | const call = endpoints[path][i] 384 | results.push(await processCall(call, req)) 385 | } 386 | var contype = req.headers['content-type'] 387 | 388 | if (req.query.output && req.query.output === 'javascript') { 389 | res.send(results) 390 | } else if ((!contype || contype.indexOf('application/json') !== 0) && withEum) { 391 | res.send(`${config.name}${JSON.stringify(results)}`) 392 | } else { 393 | res.send(results) 394 | } 395 | } catch (reason) { 396 | logger.error(reason.message) 397 | res.status(typeof reason.code === 'number' ? reason.code : 500).send(reason.message) 398 | } 399 | } else { 400 | res.status(404).send('404') 401 | } 402 | } 403 | 404 | app.get('/**', function (req, res) { 405 | processRequest(req, res, req.query) 406 | }) 407 | 408 | app.post('/**', function (req, res) { 409 | processRequest(req, res, req.body) 410 | }) 411 | 412 | var server = app.listen(port, () => console.log( 413 | `Running ${config.name} (type: ${config.type}) on port ${port} with${config.agent === 'yes' 414 | ? ` agent, reporting to ${apm.controller}` 415 | : 'out agent'}`)) 416 | 417 | if (config.hasOwnProperty('options')) { 418 | server.on('connection', (socket) => { 419 | if (config.options.hasOwnProperty('connectionDelay')) { 420 | sleep.msleep(config.options.connectionDelay) 421 | } 422 | if (config.options.hasOwnProperty('lossRate') && parseFloat(config.options.lossRate) >= Math.random()) { 423 | socket.end() 424 | throw new Error('An error occurred') 425 | } 426 | }) 427 | } 428 | -------------------------------------------------------------------------------- /nodes/nodejs/node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$WITH_AGENT" -eq "1" ] 3 | then 4 | echo "Running with agent..." 5 | # The additional "-Dappdynamics.dockerMonitoring=true" is only a hint for the machine agent to add this container to docker monitoring. 6 | node index.js 80 -Dappdynamics.dockerMonitoring=true 7 | else 8 | echo "Running without agent..." 9 | node index.js 80 10 | fi 11 | -------------------------------------------------------------------------------- /nodes/nodejs/otel-tracing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { LogLevel } = require('@opentelemetry/core') 3 | const { NodeTracerProvider } = require('@opentelemetry/node') 4 | const { BatchSpanProcessor } = require('@opentelemetry/tracing') 5 | const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin') 6 | 7 | const config = JSON.parse(process.env.APP_CONFIG) 8 | 9 | const provider = new NodeTracerProvider({ 10 | logLevel: LogLevel.DEBUG 11 | }) 12 | 13 | provider.register() 14 | 15 | const exporter = new ZipkinExporter({ 16 | serviceName: config.name, 17 | url: 'http://opentelemetry-collector:9411/' 18 | // If you are running your tracing backend on another host, 19 | // you can point to it using the `url` parameter of the 20 | // exporter config. 21 | }) 22 | provider.addSpanProcessor(new BatchSpanProcessor(exporter, { 23 | // send spans as soon as we have this many 24 | bufferSize: 2, 25 | // send spans if we have buffered spans older than this 26 | bufferTimeout: 50 27 | })) 28 | 29 | console.log('tracing initialized') 30 | -------------------------------------------------------------------------------- /nodes/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appdynamics": "^20.12.0", 4 | "chance": "^1.1.3", 5 | "cronmatch": "^0.1.1", 6 | "express": "^4.17.1", 7 | "log4js": "^3.0.6", 8 | "morgan": "^1.9.1", 9 | "request": "^2.88.2", 10 | "request-promise": "^4.2.4", 11 | "sleep": "^6.3.0", 12 | "@opentelemetry/core": "^0.11.0", 13 | "@opentelemetry/exporter-collector": "^0.16.0", 14 | "@opentelemetry/exporter-prometheus": "^0.11.0", 15 | "@opentelemetry/exporter-zipkin": "^0.11.0", 16 | "@opentelemetry/metrics": "^0.11.0", 17 | "@opentelemetry/node": "^0.11.0", 18 | "@opentelemetry/plugin-express": "^0.10.0", 19 | "@opentelemetry/plugin-http": "^0.11.0", 20 | "@opentelemetry/plugin-https": "^0.11.0", 21 | "@opentelemetry/tracing": "^0.11.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^6.2.1", 25 | "eslint-config-standard": "^14.0.0", 26 | "eslint-plugin-import": "^2.18.2", 27 | "eslint-plugin-json": "^1.4.0", 28 | "eslint-plugin-node": "^9.1.0", 29 | "eslint-plugin-promise": "^4.2.1", 30 | "eslint-plugin-standard": "^4.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /nodes/nodejs/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | env CUSTOM_CODE_DIR="../../scripts" APP_CONFIG="$(<../frontend.json)" APM_CONFIG="$(<../appdynamics.json)" nodemon index.js 8000 3 | -------------------------------------------------------------------------------- /nodes/php/.gitignore: -------------------------------------------------------------------------------- 1 | appdynamics-php-agent-x64-linux-* 2 | -------------------------------------------------------------------------------- /nodes/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-apache 2 | COPY appdynamics-php-agent-x64-linux-*.tar.bz2 /tmp/ 3 | RUN mkdir -p /opt/appdynamics/appdynamics-php-agent; tar xvfj /tmp/appdynamics-php-agent-x64-linux-*.tar.bz2 -C /opt/appdynamics/appdynamics-php-agent --strip 1 4 | RUN /opt/appdynamics/appdynamics-php-agent/install.sh -s -a ACCOUNTNAME@ACCESSKEY CONTROLLERHOST CONTROLLERPORT APPLICATIONNAME TIERNAME NODENAME 5 | RUN a2enmod rewrite 6 | RUN docker-php-ext-install pdo pdo_mysql 7 | COPY entrypoint.sh /usr/local/bin/ 8 | COPY agent-setup.php /usr/local/bin 9 | RUN chmod +x /usr/local/bin/entrypoint.sh 10 | COPY index.php /var/www/html 11 | COPY info.php /var/www/html 12 | COPY htaccess.txt /var/www/html/.htaccess 13 | COPY appdynamics_agent.ini /usr/local/etc/php/conf.d/ 14 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 15 | EXPOSE 80 16 | CMD ["apache2-foreground"] 17 | -------------------------------------------------------------------------------- /nodes/php/agent-setup.php: -------------------------------------------------------------------------------- 1 | controller); 8 | 9 | $keys = [ 10 | 'CONTROLLERHOST' => $controller['host'], 11 | 'CONTROLLERPORT' => $controller['port'], 12 | 'APPLICATIONNAME' => $config->applicationName, 13 | 'TIERNAME' => $service->name, 14 | 'NODENAME' => $service->name.'-'.gethostname(), 15 | 'NODEREUSEPREFIX' => $service->name, 16 | 'ACCOUNTNAME' => $config->accountName, 17 | 'ACCESSKEY' => $config->accountAccessKey 18 | ]; 19 | 20 | foreach($keys as $key => $value) { 21 | $file = str_replace($key, $value, $file); 22 | } 23 | 24 | $sslEnabled = $controller['scheme'] === 'https'; 25 | 26 | if($sslEnabled) { 27 | $file = str_replace('agent.controller.ssl.enabled = 0', 'agent.controller.ssl.enabled = 1', $file); 28 | } else { 29 | $file = str_replace('agent.controller.ssl.enabled = 1', 'agent.controller.ssl.enabled = 0', $file); 30 | } 31 | 32 | file_put_contents($argv[1], $file); 33 | -------------------------------------------------------------------------------- /nodes/php/appdynamics_agent.ini: -------------------------------------------------------------------------------- 1 | [AppDynamics Agent] 2 | extension = appdynamics_agent.so 3 | agent.log4cxx_config = /opt/appdynamics/appdynamics-php-agent/php/conf/appdynamics_agent_log4cxx.xml 4 | agent.php_agent_root = /opt/appdynamics/appdynamics-php-agent 5 | agent.controller.hostName = CONTROLLERHOST 6 | agent.controller.port = CONTROLLERPORT 7 | agent.applicationName = APPLICATIONNAME 8 | agent.tierName = TIERNAME 9 | agent.nodeName = NODENAME 10 | agent.nodeReuse = true 11 | agent.nodeReusePrefix = NODEREUSEPREFIX 12 | agent.controller.ssl.enabled = 1 13 | agent.accountName = ACCOUNTNAME 14 | agent.accountAccessKey = ACCESSKEY 15 | agent.analyticsHostName = machine-agent 16 | agent.analyticsPort = 9090 17 | agent.auto_launch_proxy = 0 18 | agent.proxy_ctrl_dir = /phpproxy/ 19 | -------------------------------------------------------------------------------- /nodes/php/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "$WITH_AGENT" -eq "1" ] 5 | then 6 | echo "[PHP] Running with agent..." 7 | php /usr/local/bin/agent-setup.php /usr/local/etc/php/conf.d/appdynamics_agent.ini 8 | else 9 | echo "[PHP] Running without agent..." 10 | rm /usr/local/etc/php/conf.d/appdynamics_agent.ini 11 | fi 12 | 13 | # first arg is `-f` or `--some-option` 14 | if [ "${1#-}" != "$1" ]; then 15 | set -- apache2-foreground "$@" 16 | fi 17 | 18 | exec "$@" 19 | -------------------------------------------------------------------------------- /nodes/php/htaccess.txt: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule ^(.*)$ /index.php?path=$1 [NC,L,QSA] 5 | -------------------------------------------------------------------------------- /nodes/php/index.php: -------------------------------------------------------------------------------- 1 | eum)) { 16 | $withEum = true; 17 | $eumConfig = $apmConfig->eum; 18 | 19 | $eumConfig->xd = ["enable" => false]; 20 | } 21 | 22 | function startsWith($haystack, $needle) 23 | { 24 | $length = strlen($needle); 25 | return (substr($haystack, 0, $length) === $needle); 26 | } 27 | 28 | function loadFromCache($timeout) { 29 | $start = microtime(true); 30 | $finish = $start; 31 | $response = ""; 32 | while($finish - $start < $timeout/1000) { 33 | $exitCall = appdynamics_begin_exit_call(AD_EXIT_CACHE, 'Redis Cache', ['VENDOR' => 'Redis', "SERVER POOL" => 'redis:6380'], false); 34 | usleep(1000 * rand(100,200)); 35 | if(!is_null($exitCall)) { 36 | appdynamics_end_exit_call($exitCall); 37 | } 38 | $finish = microtime(true); 39 | } 40 | return ($finish - $start). " loaded from cache"; 41 | } 42 | 43 | function buildResponse($timeout) { 44 | $start = microtime(true); 45 | $finish = $start; 46 | $response = ""; 47 | while($finish - $start < $timeout/1000) { 48 | $response .= " "; 49 | $finish = microtime(true); 50 | } 51 | return strlen($response) . " slow response"; 52 | } 53 | 54 | function queryDatabase($url) { 55 | 56 | $hostName = $url['host']; 57 | $database = substr($url['path'], 1); 58 | 59 | parse_str($url['query'], $query); 60 | $query = $query['query']; 61 | 62 | try { 63 | $dbh = new PDO('mysql:dbname='.$database.';host='.$hostName, 'root', 'root'); 64 | } catch (PDOException $e) { 65 | return 'Connection failed: ' . $e->getMessage(); 66 | } 67 | 68 | $dbh->query($query); 69 | 70 | return "Database query '".$query."' executed on ".$database."@".$hostName; 71 | } 72 | 73 | function processCall($call) { 74 | 75 | $remoteTimeout = false; 76 | $catchExceptions = true; 77 | 78 | if(is_array($call)) { 79 | shuffle($call); 80 | $call = $call[0]; 81 | } 82 | if(is_object($call)) { 83 | if(isset($call->probability) && $call->probability * 100 <= rand(0, 100)) { 84 | return $call->call." was not probable"; 85 | } 86 | if(isset($call->remoteTimeout)) { 87 | $remoteTimeout = $call->remoteTimeout; 88 | } 89 | if(isset($call->catchExceptions)) { 90 | $catchExceptions = $call->catchExceptions; 91 | } 92 | $call = $call->call; 93 | } 94 | if(startsWith($call, 'sleep')) { 95 | $timeout = explode(',', $call)[1]; 96 | usleep($timeout * 1000); 97 | return "Slept for ${timeout}"; 98 | } elseif(startsWith($call, 'slow')) { 99 | $timeout = explode(',', $call)[1]; 100 | return buildResponse($timeout); 101 | } elseif(startsWith($call, 'error')) { 102 | $error = explode(',', $call); 103 | throw new Exception($error[2], $error[1]); 104 | } elseif(startsWith($call, 'image')) { 105 | $src = explode(',', $call)[1]; 106 | return ""; 107 | } elseif(startsWith($call, 'http')) { 108 | $ch = curl_init(); 109 | curl_setopt($ch, CURLOPT_URL, $call); 110 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 111 | if(is_numeric($remoteTimeout)) { 112 | curl_setopt($ch, CURLOPT_TIMEOUT_MS, $remoteTimeout); 113 | } 114 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); 115 | $r = curl_exec($ch); 116 | if(curl_errno($ch)) { 117 | if(!$catchExceptions) { 118 | throw new Exception(curl_error($ch), 500); 119 | } 120 | return curl_error($ch); 121 | } 122 | return $r; 123 | } elseif(startsWith($call, 'cache')) { 124 | $timeout = explode(',', $call)[1]; 125 | return loadFromCache($timeout); 126 | } elseif(startsWith($call, 'sql')) { 127 | return queryDatabase(parse_url($call)); 128 | } 129 | return "${call} is not supported"; 130 | } 131 | 132 | $parsed_url = parse_url($_SERVER['REQUEST_URI']); 133 | $config = json_decode($_ENV['APP_CONFIG']); 134 | 135 | $endpoint = $parsed_url['path']; 136 | $endpoints = $config->endpoints->http; 137 | 138 | foreach ($endpoints as $key => $value) { 139 | if(!startsWith($key, '/')) { 140 | $newKey = '/' . $key; 141 | $endpoints->$newKey = $value; 142 | } 143 | } 144 | 145 | if(property_exists($endpoints, $endpoint)) { 146 | try { 147 | $result = array_map('processCall', $endpoints->$endpoint); 148 | 149 | if($_SERVER['CONTENT_TYPE'] && $_SERVER['CONTENT_TYPE'] === 'application/json') { 150 | $withEum = false; 151 | } 152 | 153 | if($withEum) { 154 | echo "" . $name . "".json_encode($result); 155 | } else { 156 | echo json_encode($result); 157 | } 158 | } catch(Exception $e) { 159 | http_response_code($e->getCode()); 160 | echo $e->getMessage(); 161 | } 162 | } else { 163 | http_response_code(404); 164 | echo 404; 165 | } 166 | -------------------------------------------------------------------------------- /nodes/php/info.php: -------------------------------------------------------------------------------- 1 | status = opcache_get_status(); 8 | } 9 | function run() { 10 | return $this; 11 | } 12 | 13 | function __get($value) { 14 | if(array_key_exists($value, $this->status)) { 15 | $result = $this->status[$value]; 16 | if(is_array($result)) { 17 | return (object)$result; 18 | } 19 | return $result; 20 | } 21 | return 'not found'; 22 | } 23 | 24 | function get($value, $d = '.') { 25 | $v = explode($d, $value); 26 | $a = $this->status; 27 | foreach($v as $e) { 28 | $a = $a[$e]; 29 | } 30 | return $a; 31 | } 32 | 33 | function __toString() { 34 | return json_encode($this->status); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/php/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | env APP_CONFIG="$(<../frontend.json)" APM_CONFIG="$(<../appdynamics.json)" php -S 127.0.0.1:9001 3 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | WITH_BUILD=1 3 | DEFAULT_FILE_NAME="defaults.yml" 4 | VERBOSITY=1 5 | 6 | while getopts ":nqd:" opt; do 7 | case $opt in 8 | n) 9 | echo -e "\033[33mWill skip build.\033[0m" 10 | WITH_BUILD=0 11 | ;; 12 | q) 13 | VERBOSITY=0 14 | ;; 15 | d) 16 | DEFAULT_FILE_NAME=${OPTARG} 17 | if [ ! -f "${DEFAULT_FILE_NAME}" ] 18 | then 19 | echo -e "\033[31mDefault configuration '${DEFAULT_FILE_NAME}' not found. Please verify the file path\033[0m" 20 | exit 21 | fi 22 | ;; 23 | \?) 24 | echo "Unknown option ${OPTARG}" >&2 25 | ;; 26 | esac 27 | done 28 | shift $((OPTIND-1)) 29 | 30 | CONFIG=$1 31 | CONTAINER_PREFIX=$2 32 | 33 | if [ -z "${CONFIG}" ] 34 | then 35 | CONTAINER_PREFIX="apm-game" 36 | CONFIG=config.yml 37 | fi 38 | 39 | if [ ! -f "${CONFIG}" ] 40 | then 41 | echo -e "\033[31mConfiguration '${CONFIG}' not found. Please verify the file path\033[0m" 42 | exit 43 | fi 44 | 45 | if [ -z "${CONTAINER_PREFIX}" ] 46 | then 47 | CONTAINER_PREFIX=`basename ${CONFIG%.yml}` 48 | fi 49 | 50 | CUSTOM_CODE_DIR="$(pwd)/scripts" 51 | IMAGE_PREFIX="apm-game" 52 | DOCKER_NETWORK="${CONTAINER_PREFIX}/network" 53 | DOCKER_LOGS_VOLUME="${CONTAINER_PREFIX}-logs" 54 | DOCKER_PHP_PROXY_VOLUME="${CONTAINER_PREFIX}-php-proxy" 55 | 56 | IS_RUNNING=`docker ps -f name=${CONTAINER_PREFIX} -q` 57 | 58 | if [ -n "${IS_RUNNING}" ] 59 | then 60 | echo "${CONTAINER_PREFIX} is already in use. If you want to run the same application twice, provide a container prefix as second parameter:" 61 | echo -e "\n\t${0} ${1} ${CONTAINER_PREFIX}-2\n" 62 | exit 63 | fi 64 | 65 | if [ "${WITH_BUILD}" -eq "1" ] 66 | then 67 | chmod +x ./build.sh 68 | ./build.sh "${IMAGE_PREFIX}" 69 | fi 70 | 71 | docker network create ${DOCKER_NETWORK} 72 | docker volume create ${DOCKER_LOGS_VOLUME} 73 | docker volume create ${DOCKER_PHP_PROXY_VOLUME} 74 | 75 | NETWORK_DETAILS=$(docker network inspect ${DOCKER_NETWORK}) 76 | 77 | node master/index.js "${CONFIG}" "${IMAGE_PREFIX}" "${DOCKER_NETWORK}" "${DOCKER_LOGS_VOLUME}" "${CONTAINER_PREFIX}" "${CUSTOM_CODE_DIR}" "${NETWORK_DETAILS}" "${DEFAULT_FILE_NAME}" "${VERBOSITY}" "${DOCKER_PHP_PROXY_VOLUME}" 78 | 79 | docker network rm ${DOCKER_NETWORK} 80 | docker volume rm ${DOCKER_LOGS_VOLUME} 81 | docker volume rm ${DOCKER_PHP_PROXY_VOLUME} 82 | -------------------------------------------------------------------------------- /scripts/search.js: -------------------------------------------------------------------------------- 1 | /* 2 | Use the following sample script as template for your custom scripts used with 3 | nodejs and a "script," command. 4 | 5 | The parameter "$" comes with the following helpers: 6 | 7 | - txn: The appdynamics transaction (see https://docs.appdynamics.com/display/PRO45/Node.js+Agent+API+Reference) 8 | - logger: A log4js logger object (learn more at https://log4js-node.github.io/log4js-node/) 9 | - add: A helper method to add data to analytics/snapshots 10 | - sleep(n): A helper to stop the event loop for n milliseconds 11 | - chance: Generator for randomness (learn more at https://chancejs.com/) 12 | 13 | For examples see below 14 | 15 | */ 16 | module.exports = function($) { 17 | 18 | // Log with log level "info" that the script starts 19 | $.logger.info("Search script executed") 20 | 21 | // Add the price to snapshot&analytics data 22 | $.add("limit", 1000) 23 | 24 | // Add a random name to snapshot&analytics data 25 | $.add("keyword", $.chance.name()) 26 | 27 | // With a 30% chance the execution is slowed down by 1.5 seconds 28 | if(chance.bool({likelihood: 30})) { 29 | $.sleep.msleep(1500) 30 | } 31 | 32 | // With another 30% chance, we return an error 33 | if($.chance.bool({likelihood: 30})) { 34 | return {code: 500, message: "Oops!"} 35 | } 36 | 37 | // The "add" method is just a wrapper around the AppDynamics API, with "txn" you can call those functions yourself 38 | $.txn.addAnalyticsData("resultSize", 155) 39 | 40 | // Log with log level "debug" that the script ends 41 | $.logger.debug("Search script ended") 42 | 43 | // Will write "Hello World" to the output 44 | return "Hello World" 45 | } 46 | -------------------------------------------------------------------------------- /tutorial/step-1.yml: -------------------------------------------------------------------------------- 1 | apm: 2 | applicationName: apm-game-tutorial 3 | controller: 4 | accountName: 5 | accountAccessKey: 6 | eventsService: 7 | globalAccountName: 8 | 9 | services: 10 | frontend: 11 | type: java 12 | agent: yes 13 | endpoints: 14 | http: 15 | /list: 16 | - http://backend/list/items 17 | - cache,128 18 | backend: 19 | type: java 20 | agent: yes 21 | endpoints: 22 | http: 23 | /list/items: 24 | - slow,1024 25 | 26 | loaders: 27 | browser: 28 | type: puppeteer 29 | wait: 15 30 | count: 1 31 | urls: 32 | - http://frontend/list 33 | -------------------------------------------------------------------------------- /tutorial/step-2.yml: -------------------------------------------------------------------------------- 1 | apm: 2 | applicationName: apm-game-tutorial 3 | controller: 4 | accountName: 5 | accountAccessKey: 6 | eventsService: 7 | globalAccountName: 8 | 9 | services: 10 | frontend: 11 | type: java 12 | agent: yes 13 | endpoints: 14 | http: 15 | /list: 16 | - http://backend/list/items 17 | - cache,128 18 | backend: 19 | type: java 20 | agent: yes 21 | endpoints: 22 | http: 23 | /list/items: 24 | - slow,524 25 | - http://storage/item 26 | new-frontend: 27 | type: nodejs 28 | agent: yes 29 | endpoints: 30 | http: 31 | /list: 32 | - http://backend/list/items 33 | - cache,128 34 | storage: 35 | type: nodejs 36 | agent: no 37 | endpoints: 38 | http: 39 | /item: 40 | - sleep,500 41 | 42 | loaders: 43 | browser: 44 | type: puppeteer 45 | wait: 15 46 | count: 1 47 | urls: 48 | - http://frontend/list 49 | - http://new-frontend/list 50 | -------------------------------------------------------------------------------- /tutorial/step-3.yml: -------------------------------------------------------------------------------- 1 | apm: 2 | applicationName: apm-game-tutorial 3 | controller: 4 | accountName: 5 | accountAccessKey: 6 | eventsService: 7 | globalAccountName: 8 | 9 | services: 10 | frontend: 11 | type: java 12 | agent: yes 13 | endpoints: 14 | http: 15 | /list: 16 | - http://backend/list/items 17 | - cache,128 18 | backend: 19 | type: java 20 | agent: yes 21 | endpoints: 22 | http: 23 | /list/items: 24 | - slow,524 25 | - call: error,500,Oops 26 | probability: 0.1 27 | - http://storage/item 28 | new-backend: 29 | type: nodejs 30 | agent: yes 31 | endpoints: 32 | http: 33 | /list/items: 34 | - cache,50 35 | new-frontend: 36 | type: nodejs 37 | agent: yes 38 | endpoints: 39 | http: 40 | /list: 41 | - [http://backend/list/items, http://new-backend/list/items] 42 | - cache,128 43 | storage: 44 | type: nodejs 45 | agent: no 46 | endpoints: 47 | http: 48 | /item: 49 | - sleep,500 50 | 51 | 52 | loaders: 53 | browser: 54 | type: puppeteer 55 | wait: 15 56 | count: 1 57 | urls: 58 | - http://frontend/list 59 | - http://new-frontend/list 60 | -------------------------------------------------------------------------------- /tutorial/step-4.yml: -------------------------------------------------------------------------------- 1 | apm: 2 | applicationName: apm-game-tutorial 3 | controller: 4 | accountName: 5 | accountAccessKey: 6 | eventsService: 7 | globalAccountName: 8 | eum: 9 | appKey: 10 | adrumExtUrlHttp: 'http://cdn.appdynamics.com' 11 | adrumExtUrlHttps: 'https://cdn.appdynamics.com' 12 | beaconUrlHttp: 'http://col.eum-appdynamics.com' 13 | beaconUrlHttps: 'https://col.eum-appdynamics.com' 14 | 15 | services: 16 | frontend: 17 | type: java 18 | agent: yes 19 | endpoints: 20 | http: 21 | /list: 22 | - http://backend/list/items 23 | - cache,128 24 | backend: 25 | type: java 26 | agent: yes 27 | endpoints: 28 | http: 29 | /list/items: 30 | - slow,524 31 | - call: error,500,Oops 32 | probability: 0.1 33 | - http://storage/item 34 | new-backend: 35 | type: nodejs 36 | agent: yes 37 | endpoints: 38 | http: 39 | /list/items: 40 | - cache,50 41 | new-frontend: 42 | type: nodejs 43 | agent: yes 44 | endpoints: 45 | http: 46 | /list: 47 | - [http://backend/list/items, http://new-backend/list/items] 48 | - cache,128 49 | storage: 50 | type: nodejs 51 | agent: no 52 | endpoints: 53 | http: 54 | /item: 55 | - sleep,500 56 | 57 | 58 | loaders: 59 | browser: 60 | type: puppeteer 61 | wait: 15 62 | count: 1 63 | urls: 64 | - http://frontend/list 65 | - http://new-frontend/list 66 | -------------------------------------------------------------------------------- /tutorial/step-5.yml: -------------------------------------------------------------------------------- 1 | apm: 2 | applicationName: apm-game-tutorial-ecommerce 3 | controller: 4 | accountName: 5 | accountAccessKey: 6 | eventsService: 7 | globalAccountName: 8 | eum: 9 | appKey: 10 | adrumExtUrlHttp: 'http://cdn.appdynamics.com' 11 | adrumExtUrlHttps: 'https://cdn.appdynamics.com' 12 | beaconUrlHttp: 'http://col.eum-appdynamics.com' 13 | beaconUrlHttps: 'https://col.eum-appdynamics.com' 14 | 15 | services: 16 | mobile-frontend: 17 | type: nodejs 18 | agent: yes 19 | endpoints: 20 | http: 21 | /product: 22 | - http://backend/item 23 | - call: data 24 | id: product 25 | value: APM Game - Limited Edition 26 | /add-to-cart: 27 | - http://backend/cart/add 28 | - call: data 29 | id: price 30 | value: 35 31 | /checkout: 32 | - http://backend/cart/checkout 33 | - call: data 34 | id: name 35 | chance: name 36 | - call: data 37 | id: ccNumber 38 | chance: cc,type:Mastercard 39 | www-frontend: 40 | type: java 41 | agent: yes 42 | endpoints: 43 | http: 44 | /product: 45 | - http://backend/item 46 | - call: data 47 | id: product 48 | type: string 49 | value: APM Game - Limited Edition 50 | /add-to-cart: 51 | - http://backend/cart/add 52 | - call: data 53 | id: price 54 | type: double 55 | value: 35 56 | /checkout: 57 | - http://backend/cart/checkout 58 | - call: data 59 | id: name 60 | type: string 61 | value: [Dafi Vatemi, Nelgatwu Powuku Heup, Patrick Copeland] 62 | backend: 63 | type: nodejs 64 | agent: no 65 | endpoints: 66 | http: 67 | /item: 68 | - log,info,Item available 69 | - sleep,50 70 | /cart/add: 71 | - log,warn,Cart is full 72 | - sleep,150 73 | /cart/checkout: 74 | - log,debug,Checkout 75 | - sleep,500 76 | 77 | 78 | loaders: 79 | mobile: 80 | type: puppeteer 81 | wait: 15 82 | count: 1 83 | urls: 84 | - http://mobile-frontend/product 85 | - http://mobile-frontend/add-to-cart 86 | - http://mobile-frontend/checkout 87 | browser: 88 | type: puppeteer 89 | wait: 15 90 | count: 1 91 | urls: 92 | - http://www-frontend/product 93 | - http://www-frontend/add-to-cart 94 | - http://www-frontend/checkout 95 | -------------------------------------------------------------------------------- /tutorial/step-6.yml: -------------------------------------------------------------------------------- 1 | global: 2 | machine: false 3 | 4 | apm: 5 | applicationName: apm-game-tutorial-dynamic-attach 6 | controller: 7 | accountName: 8 | accountAccessKey: 9 | eventsService: 10 | globalAccountName: 11 | 12 | services: 13 | frontend: 14 | type: java 15 | agent: no 16 | endpoints: 17 | http: 18 | /list: 19 | - http://backend/list/items 20 | - cache,128 21 | backend: 22 | type: java 23 | agent: no 24 | endpoints: 25 | http: 26 | /list/items: 27 | - slow,1024 28 | 29 | loaders: 30 | browser: 31 | type: puppeteer 32 | wait: 15 33 | count: 1 34 | urls: 35 | - http://frontend/list 36 | --------------------------------------------------------------------------------