How to configure AWS CDK ApplicationLoadBalancedFargateService to log parsed JSON lines with Firelens and Firebit - logback

When I create an ApplicationLoadBalancedFargateService with a Firelens logdriver, and the application writes JSON lines as the log message, such as when using net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder with Logback, the log messages are displayed in my logging repository (ex. Sumo Logic), as an escaped string, like:
How can I get the log messages to save as parsed JSON?

After scanning CDK source code, browsing several related references (which I will provide links for to help direct appropriate traffic here), and using cdk diff until the only change was to enable json parsing, I was able to make is work as shown in the following code. The key here is the use of the addFirelensLogRouter method and the Firelens config contained therein.
TaskDefinition code does not automatically create a LogRouter container, if the task definition already contains one, which is what allows us to override the default behavior.
protected _createFargateService() {
const logDriver = LogDrivers.firelens({
options: {
Name: 'http',
Host: this._props.containerLogging.endpoint,
URI: this._props.containerLogging.uri,
Port: '443',
tls: 'on',
'tls.verify': 'off',
Format: 'json_lines'
}
});
const fargateService = new ApplicationLoadBalancedFargateService(this, this._props.serviceName, {
cluster: this._accountEnvironmentLookups.getComputeCluster(),
cpu: this._props.cpu, // Default is 256
desiredCount: this._props.desiredCount, // Default is 1
taskImageOptions: {
image: ContainerImage.fromEcrRepository(this._props.serviceRepository, this._props.imageVersion),
environment: this._props.environment,
containerPort: this._props.containerPort,
logDriver
},
memoryLimitMiB: this._props.memoryLimitMiB, // Default is 512
publicLoadBalancer: this._props.publicLoadBalancer, // Default is false
domainName: this._props.domainName,
domainZone: !!this._props.hostedZoneDomain ? HostedZone.fromLookup(this, 'ZoneFromLookup', {
domainName: this._props.hostedZoneDomain
}) : undefined,
certificate: !!this._props.certificateArn ? Certificate.fromCertificateArn(this, 'CertificateFromArn', this._props.certificateArn) : undefined,
serviceName: `${this._props.accountShortName}-${this._props.deploymentEnvironment}-${this._props.serviceName}`,
// The new ARN and resource ID format must be enabled to work with ECS managed tags.
//enableECSManagedTags: true,
//propagateTags: PropagatedTagSource.SERVICE,
// CloudMap properties cannot be set from a stack separate from the stack where the cluster is created.
// see https://github.com/aws/aws-cdk/issues/7825
});
if (this._props.logMessagesAreJsonLines) {
// The default log driver setup doesn't enable json line parsing.
const firelensLogRouter = fargateService.service.taskDefinition.addFirelensLogRouter('log-router', {
// Figured out how get the default fluent bit ECR image from here https://github.com/aws/aws-cdk/blob/60c782fe173449ebf912f509de7db6df89985915/packages/%40aws-cdk/aws-ecs/lib/base/task-definition.ts#L509
image: obtainDefaultFluentBitECRImage(fargateService.service.taskDefinition, fargateService.service.taskDefinition.defaultContainer?.logDriverConfig),
essential: true,
firelensConfig: {
type: FirelensLogRouterType.FLUENTBIT,
options: {
enableECSLogMetadata: true,
configFileType: FirelensConfigFileType.FILE,
// This enables parsing of log messages that are json lines
configFileValue: '/fluent-bit/configs/parse-json.conf'
}
},
memoryReservationMiB: 50,
logging: new AwsLogDriver({streamPrefix: 'firelens'})
});
firelensLogRouter.logDriverConfig;
}
fargateService.targetGroup.configureHealthCheck({
path: this._props.healthUrlPath,
port: this._props.containerPort.toString(),
interval: Duration.seconds(120),
unhealthyThresholdCount: 5
});
const scalableTaskCount = fargateService.service.autoScaleTaskCount({
minCapacity: this._props.desiredCount,
maxCapacity: this._props.maxCapacity
});
scalableTaskCount.scaleOnCpuUtilization(`ScaleOnCpuUtilization${this._props.cpuTargetUtilization}`, {
targetUtilizationPercent: this._props.cpuTargetUtilization
});
scalableTaskCount.scaleOnMemoryUtilization(`ScaleOnMemoryUtilization${this._props.memoryTargetUtilization}`, {
targetUtilizationPercent: this._props.memoryTargetUtilization
});
this.fargateService = fargateService;
}
Resources:
How I first discovered it might be possible.
https://github.com/aws-samples/amazon-ecs-firelens-examples/tree/master/examples/fluent-bit/parse-json
How I discovered it might be possible with CDK https://github.com/aws/aws-cdk/pull/6322
Understanding it from an AWS service standpoint https://docs.aws.amazon.com/AmazonECS/latest/userguide/using_firelens.html
Narrowing in on where it resides in CDK source. https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_aws-ecs.FirelensLogRouter.html
Eventually I landed here and figured out https://github.com/aws/aws-cdk/blob/60c782fe173449ebf912f509de7db6df89985915/packages/%40aws-cdk/aws-ecs/lib/base/task-definition.ts#L509

Related

How to use CDK to get the IP addresses of the enis associated with a VPCE/ how to get the vpceNetworkInterfaceIds associated with a VPCE?

Background context / End goal:
I am trying to use cdk to create a target group that consists of the ip addresses that are associated with a vpc endpoint (for apigateway) as per this AWS blog.
Ideally, I would like to be able to just lookup the associated ips using just the fact that the vpce is for the service of apigateway OR potentially using the vpce id.
Attempts
I tried to use the cdk InterfaceVpcEndpoint construct static method using the fromInterfaceVpcEndpointAttributes (filtering by service). It did return the desired vpce, but unfortunately it returns in the format of IInterfaceVpcEndpoint which does not have the vpceNetworkInterfaceIds attribute that the InterfaceVpcEndpoint construct has
I was able to use AwsCustomResource (after consulting a stack overflow post that referenced this example) to look up the ip addresses for a given array of vpce network interface ids:
const vpceNetworkInterfaceIds = =['eniId1', 'eniId2'];
const getEniIps = new AwsCustomResource(scope, `GetEndpointIps`, {
onUpdate: {
service: "EC2",
action: "describeNetworkInterfaces",
parameters: {
NetworkInterfaceIds: vpceNetworkInterfaceIds
},
physicalResourceId: PhysicalResourceId.of(Date.now().toString())
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: AwsCustomResourcePolicy.ANY_RESOURCE
}),
});
const privateIpAddresses: string[] = [];
for(let i = 0; i< vpceNetworkInterfaceIds.length; i++){
const privateIpAddress: string = getNetworkInterfaceIpAddresses.getResponseField(`NetworkInterfaces.${i}.PrivateIpAddress`).toString();
privateIpAddresses.push(privateIpAddress);
}
return privateIpAddresses;
}
I tried to make a similar sdk call (describeVpcEndpoints), but then I encountered issues retrieving the array of NetworkInterfaceIds.
const getNetworkInterfaceIpAddresses = new AwsCustomResource(scope, `GetVpceNetworkInterfaceIds`, {
onUpdate: {
service: "EC2",
action: "describeVpcEndpoints",
parameters: {
Filters: [
{
Name: "service-name",
Values: ["com.amazonaws.us-east-1.execute-api"]
}
]
},
physicalResourceId: PhysicalResourceId.of(Date.now().toString())
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: AwsCustomResourcePolicy.ANY_RESOURCE
}),
});
return getNetworkInterfaceIpAddresses.getResponseFieldReference(`VpcEndpoints.0.NetworkInterfaceIds`).toJSON();
I tried variations of using the Reference methods of toJson and toString but was not able to figure out how to get the array of values from this custom resource.
Questions
How can you get an array from the sdk call of a aws custom resource?
Is there a more straight forward way to get the vpceNetworkInterfaceIds of a given vpce?
Is there a more straight forward way to get the ip addresses for a given vpce?

IPFS file extension for GLB

I'm using the ipfs-http-client module to interact with IPFS. My problem is that I need the file extension on the link that I generate, and it seems that I can only get it with the wrapWithDirectory flag (-w with the command line). But this flag makes the result empty so far. The documentation on IPFS is only about the command line, and I've only found out a few tutorials about how to do it, but with other tool than JS, or by uploading folders manually. I need to do it from a JS script, from a single file. The motivation is that I want to generate metadata for an NFT, and a metadata field requires to point to a file with a specific extension.
Full detail: I need to add a GLB file on Opensea. GLB are like GLTF, it's a standard for 3D file. Opensea can detect the animation_url field of the metadata of an NFT and render that file. But it needs to end with .glb. Translation, my NFT needs its metadata to look like that:
{
name: <name>,
description: <description>,
image: <image>,
animation_url: 'https://ipfs.io/ipfs/<hash>.glb' // Opensea requires the '.glb' ending.
}
The way I do this so far is as follows:
import { create } from 'ipfs-http-client';
const client = create({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: { authorization },
});
const result = await client.add(file); // {path: '<hash>', cid: CID}
const link = `https://ipfs.io/ipfs/${result.path}` // I can't add an extension here.
In that code, I can put animation_url: link in the metadata object, but OpenSea won't recognize it.
I have tried adding the option mentioned above as well:
const result = await client.add(file, {wrapWithDirectory: true}); // {path: '', cid: CID}
But then result.path is an empty string.
How can I generate a link ending with .glb?
Found out the solution. It indeed involves creating a directory, which is the returned CID, so that we can append the file name with its extension at the end. The result is https://ipfs.io/ipfs/<directory_hash>/<file_name_with_extension>.
So, correcting the code above it gives the following:
import { create } from 'ipfs-http-client';
const client = create({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: { authorization },
});
const content = await file.arrayBuffer(); // The file needs to be a buffer.
const result = await client.add(
{content, path: file.name},
{wrapWithDirectory: true}
);
// result.path is empty, it needs result.cid.toString(),
// and then one can manually append the file name with its extension.
const link = `https://ipfs.io/ipfs/${result.cid.toString()}/${result.name}`;

Is there any way of conecting atoti cube with ActiveUI frontend?

We are trying to connect an atoti cube the same way that is on env.js on the Active UI frontend.
window.env = {
contentServerVersion: "5.11.x",
contentServerUrl: "https://activepivot-ranch.activeviam.com:5110",
// WARNING: Changing the keys of activePivotServers will break previously saved widgets and dashboards.
// If you must do it, then you also need to update each one's serverKey attribute on your content server.
activePivotServers: {
// You can connect to as many servers as needed.
// In practice most projects only need one.
"Ranch 5.11": {
url: "https://activepivot-ranch.activeviam.com:5110",
version: "5.11.1",
},
"Ranch 5.10": {
url: "https://activepivot-ranch.activeviam.com:5100",
version: "5.10.0",
},
"Ranch 5.9": {
url: "https://activepivot-ranch.activeviam.com:5900",
version: "5.9.4",
},
"my-server": {
url: "https://localhost:9090",
version: "5.10.x",
}
},
};
but when we launch the frontend we are just give this error: 404: The resource at http://localhost:9090/atoti/pivot/rest/v5/ping was not found.
The URL in your env.js is probably not correct. You can find the correct one by running the following in your notebook:
session.link()
Let's call what it returns my-url.
Then your env.js should look like this:
window.env = {
contentServerVersion: "5.10",
contentServerUrl: my-url,
activePivotServers: {
"my-server": {
url: my-url,
version: "5.10",
},
},
};
You might also have to change your version attribute. It depends on your atoti version, as follows:
atoti 0.6.x => version = "5.11.0"
atoti 0.5.x => version = "5.10.0"
atoti 0.2.x, 0.3.x, 0.4.x => version = "5.9.0"
earlier => version = "5.8.0"

Sails Consistency violation with simple Controller attribute

In a Sails 1.x app, add a property to a controller, initialize it with a boolean, a number or null and try to lift the app.
module.exports = {
_foo: 123, // illegal
_bar: '', // legal
_baz: [] // legal
};
The lift will fail with this message:
error: Failed to lift app: Error: Consistency violation: `action` (2nd arg) should be provided as either a req/res/next function or a machine def (actions2), but instead, got: ...
However, an empty string, empty array, empty object etc. work.
Am I misunderstanding something fundamental about controllers or why are booleans and numbers not allowed?
My goal is to add simple attributes to the controller in order to temporarily store information.
Action 2 controllers follow the Node Machine structure:
https://node-machine.org/spec/machine
Helpers
For dynamic content, then the better option could be to use helpers:
https://sailsjs.com/documentation/concepts/helpers
For example:
// api/helpers/custom.js
module.exports = {
friendlyName: 'Format welcome message',
sync: true, // without this use await sails.helpers.custom
fn: function(inputs, exits, env) {
return {
data: 'example'
}
}
}
Access this info in the controller as cons data = sails.helpers.custom();.
Config
For constants, you could use a config file:
https://sailsjs.com/documentation/concepts/configuration
For example:
// config/custom.js
module.exports.custom = {
data: 'example'
}
Use this data in the controller as const data = sails.config.custom.data; // result 'example'

Quasar + Feathers-Vuex: how to integrate?

I want to integrate Quasar with FeathersJS using Feathers-Vuex
Feathers-Vuex uses a pattern to:
promise to authenticate from localStorage/cookies
.then( /*start the new Vue() app */ )
I created my app with Quasar CLI 1.0.beta16-ish and looked through /src and couldn't find the main entry point for Quasar. I feel like I'm missing something.
What includes src/store/index.js?
quasar.conf.js includes this comment - where is the main.js
// app boot file (/src/boot)
// --> boot files are part of "main.js"
boot: ["axios"],
Feathers-Vuex includes a Nuxt integration guide that may solve the same problem. These packages are all new to me, and I'm excited to learn them!
Thank you!
The part of main.js is included in quasar app.js that you can find in .quasar folder. The src/store/index.js contains the Vuex Store definition. A "store" is basically a container that holds your application state.
For more detail visit - https://quasar-framework.org/guide/app-vuex-store.html https://quasar-framework.org/guide/app-plugins.html
I ended up with two things:
Adding Feathers-Vuex to my backend.
Adding this "boot file" in my Quasar project
The comments are a bread-crumb trail if I ever have to figure it out again :-)
/*
Context:
For 3rd-party API's, we us /src/boot/axios.js
For our own API's, we use FeathersClient (socket.io & REST)
https://docs.feathersjs.com/guides/basics/clients.html
https://docs.feathersjs.com/api/authentication/client.html#appconfigureauthoptions
Our FeathersClient is in `/src/lib/feathersClient.js`
and imported into `/src/store/index.js`
which is imported by Quasar's build system. /src/quasar.conf.js setting(?)
Feathers-vuex integrates Vuex with FeathersClient:
https://feathers-vuex.feathers-plus.com/auth-module.html
Feathers-Vuex proxies it's authentication/logout actions to FeathersClient
https://github.com/feathers-plus/feathers-vuex/blob/master/src/auth-module/actions.js
The parameters for these actions are here:
https://docs.feathersjs.com/api/authentication/client.html#appauthenticateoptions
In addition to this module, you can use FeathersVuex state in UI from here:
https://feathers-vuex.feathers-plus.com/auth-module.html
This module:
Create a Feathers Auth integration for Vue as a Quasar Boot Module.
// Use case: test if user is authenticated
if (Vue.$auth.currentUser()) { ... }
// Use case: get current user's email
name = Vue.$auth.currentUser("email") || "anonymous"
// Use case: Login
Vue.$auth.login({
strategy: 'local',
email: 'my#email.com',
password: 'my-password'
});
// Use case: Logout
// logs out and sends message
let p = Vue.$auth.logout();
// After logout, go home
p.then(() => {
// User data still in browser
router.push({ name: "home"});
// To clear user data, do a hard refresh/redirect - https://feathers-vuex.feathers-plus.com/common-patterns.html#clearing-data-upon-user-logout
location && location.reload(true)
});
*/
export default ({ app, router, store, Vue }) => {
// Create the API demonstrated above
const auth = {
currentUser(prop) {
let u = store.state.auth.user || false;
if (u && prop) return u[prop];
return u;
},
login(authData, quiet) {
return store
.dispatch("auth/authenticate", authData)
.then(() => {
Vue.prototype.$q.notify({
message: "Welcome back!",
type: "info"
});
})
.catch(err => {
if (!quiet) {
console.log(err);
Vue.prototype.$q.notify({
message: "There was a problem logging you in.",
type: "error"
});
}
});
},
logout(quiet) {
return store.dispatch("auth/logout").then(() => {
if (!quiet)
Vue.prototype.$q.notify({
message: "You've been logged out.",
type: "info"
});
});
},
register(authData) {}
};
// Auth from JWT stored in browser before loading the app. true => suppress token not found error
auth.login("jwt", true);
// Add API to Vue
Vue.prototype.$auth = auth;
// If you would like to play with it in the console, uncomment this line:
// console.log(auth);
// Then, in the console:
/*
temp1.login({
strategy: "local",
email: "feathers#example.com",
password: "secret"
})
*/
// If you haven't created this user, see here:
// https://docs.feathersjs.com/guides/chat/authentication.html
// For this REST api endpoint
/*
curl 'http://localhost:3001/users/' -H 'Content-Type: application/json' --data-binary '{ "email": "feathers#example.com", "password": "secret" }'
*/
};