This commit is contained in:
2024-02-20 17:15:27 +08:00
committed by huty
parent 6706e1a633
commit 34158042ad
1529 changed files with 177765 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
FROM node:16.13.1-alpine3.14 AS builder
WORKDIR /src
COPY src/package.json .
RUN npm install
# app
FROM node:16.13.1-alpine3.14
EXPOSE 80
ENV PORT=80
CMD ["node", "server.js"]
WORKDIR /app
COPY --from=builder /src/node_modules/ /app/node_modules/
COPY src/ .

View File

@@ -0,0 +1,14 @@
const { format, transports } = require('winston');
var logConfig = module.exports = {};
logConfig.options = {
format: format.combine(
format.splat(),
format.simple()
),
transports: [
new transports.Console({
level: 'info'
})
]
};

View File

@@ -0,0 +1,5 @@
const winston = require('winston');
var logConfig = require('./config/logConfig');
const logger = winston.createLogger(logConfig.options);
exports.Logger = logger;

View File

@@ -0,0 +1,11 @@
{
"name": "access-log",
"version": "1.0.0",
"main": "server.js",
"author": "kiamol",
"dependencies": {
"restify": "8.5.1",
"winston": "3.3.3",
"prom-client": "12.0.0"
}
}

View File

@@ -0,0 +1,58 @@
const restify = require("restify");
const prom = require("prom-client");
const log = require("./log");
const os = require("os");
const accessCounter = new prom.Counter({
name: "access_log_total",
help: "Access Log - total log requests"
});
const clientIpGauge = new prom.Gauge({
name: "access_client_ip_current",
help: "Access Log - current unique IP addresses"
});
//setup Prometheus with standard metrics:
prom.collectDefaultMetrics();
function stats(req, res, next) {
log.Logger.debug("** GET /stats called");
var data = {
logs: logCount
};
res.send(data);
next();
}
function respond(req, res, next) {
log.Logger.debug("** POST /access-log called");
log.Logger.info("Access log, client IP: %s", req.body.clientIp);
logCount++;
//metrics:
accessCounter.inc();
ipAddresses.push(req.body.clientIp);
let uniqueIps = Array.from(new Set(ipAddresses));
clientIpGauge.set(uniqueIps.length);
res.send(201, "Created");
next();
}
var logCount = 0;
var ipAddresses = new Array();
var server = restify.createServer();
server.use(restify.plugins.bodyParser());
server.get("/stats", stats);
server.post("/access-log", respond);
server.get("/metrics", function(req, res, next) {
res.end(prom.register.metrics());
});
server.headersTimeout = 20;
server.keepAliveTimeout = 10;
server.listen(process.env.PORT, function() {
log.Logger.info("%s listening at %s", server.name, server.url);
});

View File

@@ -0,0 +1,11 @@
version: "3.7"
services:
ch14-access-log:
image: kiamol/ch14-access-log:latest-linux-amd64
ch14-image-gallery:
image: kiamol/ch14-image-gallery:latest-linux-amd64
ch14-image-of-the-day:
image: kiamol/ch14-image-of-the-day:latest-linux-amd64

View File

@@ -0,0 +1,11 @@
version: "3.7"
services:
ch14-access-log:
image: kiamol/ch14-access-log:latest-linux-arm64
ch14-image-gallery:
image: kiamol/ch14-image-gallery:latest-linux-arm64
ch14-image-of-the-day:
image: kiamol/ch14-image-of-the-day:latest-linux-arm64

View File

@@ -0,0 +1,17 @@
version: "3.7"
services:
ch14-access-log:
image: kiamol/ch14-access-log:latest
build:
context: ./access-log
ch14-image-gallery:
image: kiamol/ch14-image-gallery:latest
build:
context: ./image-gallery
ch14-image-of-the-day:
image: kiamol/ch14-image-of-the-day:latest
build:
context: ./image-of-the-day

View File

@@ -0,0 +1,21 @@
FROM golang:1.15-alpine AS builder
ENV CGO_ENABLED=0
WORKDIR /src
COPY go.mod .
RUN go mod download
COPY main.go .
RUN go build -o /server
# app
FROM alpine:3.15
ENV IMAGE_API_URL="http://apod-api/image" \
ACCESS_API_URL="http://apod-log/access-log"
CMD ["/web/server"]
WORKDIR /web
COPY index.html .
COPY --from=builder /server .

View File

@@ -0,0 +1,5 @@
module kiamol/image-gallery
go 1.15
require github.com/prometheus/client_golang v1.7.1

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Image Gallery</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
crossorigin="anonymous"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"/>
</head>
<body>
<div class="text-center">
<h1 class="display-4">Image Gallery</h1>
<h2>{{.Caption}}</h2>
<img src="{{.Url}}"/>
<footer class="blockquote-footer">&copy; {{.Copyright}}</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
package main
import (
"bytes"
"encoding/json"
"html/template"
"io/ioutil"
"math/rand"
"net/http"
"os"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/client_golang/prometheus"
)
type Image struct {
Url string `json:"url"`
Caption string `json:"caption"`
Copyright string `json:"copyright"`
}
type AccessLog struct {
ClientIP string `json:"clientIp"`
}
func main() {
tmpl := template.Must(template.ParseFiles("index.html"))
imageApiUrl := os.Getenv("IMAGE_API_URL")
logApiUrl := os.Getenv("ACCESS_API_URL")
//re-use HTTP client with minimal keep-alive
tr := &http.Transport{
MaxIdleConns: 1,
IdleConnTimeout: 1 * time.Second,
}
client := &http.Client{Transport: tr}
//create Prometheus metrics
inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "image_gallery_in_flight_requests",
Help: "Image Gallery - in-flight requests",
})
requestCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "image_gallery_requests_total",
Help: "Image Gallery - total requests",
},
[]string{"code", "method"},
)
prometheus.MustRegister(inFlightGauge, requestCounter)
rand.Seed(time.Now().UnixNano())
indexHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response,_ := client.Get(imageApiUrl)
defer response.Body.Close()
data,_ := ioutil.ReadAll(response.Body)
image := Image{}
json.Unmarshal([]byte(data), &image)
tmpl.Execute(w, image)
log := AccessLog{
ClientIP: r.RemoteAddr,
}
jsonLog,_ := json.Marshal(log)
response,_ = client.Post(logApiUrl, "application/json", bytes.NewBuffer(jsonLog))
defer response.Body.Close()
})
wrappedIndexHandler := promhttp.InstrumentHandlerInFlight(inFlightGauge,
promhttp.InstrumentHandlerCounter(requestCounter, indexHandler))
http.Handle("/", wrappedIndexHandler)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":80", nil)
}

View File

@@ -0,0 +1,17 @@
FROM maven:3.6.3-jdk-11 AS builder
WORKDIR /usr/src/iotd
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY . .
RUN mvn package
# app
FROM openjdk:11.0.11-jre-slim
WORKDIR /app
COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .
EXPOSE 80
ENTRYPOINT ["java", "-jar", "/app/iotd-service-0.1.0.jar"]

View File

@@ -0,0 +1,23 @@
Facade over NASA Astronomy Picture of the Day (APOD)
ENV for API_KEY
```
https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY
```
Full returns:
```
{"copyright":"Michel Loic","date":"2019-06-28","explanation":"The night of June 21 was the shortest night for planet Earth's northern latitudes, so at latitude 48.9 degrees north, Paris was no exception. Still, the City of Light had an exceptionally luminous evening. Its skies were flooded with silvery night shining or noctilucent clouds after the solstice sunset. Hovering at the edge of space, the icy condensations on meteoric dust or volcanic ash are still in full sunlight at the extreme altitudes of the mesophere. Seen at high latitudes in summer months, stunning, wide spread displays of northern noctilucent clouds are now being reported.","hdurl":"https://apod.nasa.gov/apod/image/1906/D7X7411-2Loic.jpg","media_type":"image","service_version":"v1","title":"A Solstice Night in Paris","url":"https://apod.nasa.gov/apod/image/1906/D7X7411-2Loic_1024.jpg"}
```
facade returns:
```
{
"url":""
"caption":""
"copyright":""
}
```

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sixeyed.diamol</groupId>
<artifactId>iotd-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,20 @@
package iotd;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class Application {
@RequestMapping("/")
public String home() {
return "Nothing to see here, try /image";
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,11 @@
package iotd;
import org.springframework.context.annotation.Bean;
public class BeanConfiguration {
@Bean
public CacheService cacheService() {
return new MemoryCacheService();
}
}

View File

@@ -0,0 +1,17 @@
package iotd;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
@Configuration
@EnableAspectJAutoProxy
public class RegistryConfiguration {
@Bean
TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}

View File

@@ -0,0 +1,55 @@
package iotd;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ImageController {
private static final Logger log = LoggerFactory.getLogger(ImageController.class);
@Autowired
CacheService cacheService;
@Autowired
MeterRegistry registry;
@Value("${apod.url}")
private String apodUrl;
@Value("${apod.key}")
private String apodKey;
@RequestMapping("/image")
@Timed()
public Image get() {
log.debug("** GET /image called");
Image img = cacheService.getImage();
if (img == null) {
RestTemplate restTemplate = new RestTemplate();
ApodImage result = restTemplate.getForObject(apodUrl+apodKey, ApodImage.class);
log.info("Fetched new APOD image from NASA");
registry.counter("iotd_api_image_load", "status", "success").increment();
img = new Image(result.getUrl(), result.getTitle(), result.getCopyright());
cacheService.putImage(img);
}
else {
log.debug("Loaded APOD image from cache");
registry.counter("iotd_api_image_load", "status", "cached").increment();
}
return img;
}
}

View File

@@ -0,0 +1,32 @@
package iotd;
public class ApodImage {
private String url;
private String title;
private String copyright;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getCopyright() {
return copyright;
}
public void setCopyright(String copyright) {
this.copyright = copyright;
}
}

View File

@@ -0,0 +1,47 @@
package iotd;
public class Image {
private String url;
private String caption;
private String copyright;
public Image() {}
public Image(String url, String caption, String copyright) {
setUrl(url);
setCaption(caption);
setCopyright(copyright);
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
// if it's a YouTube link need to format the URL to get the thumbnail:
// https://www.youtube.com/embed/ts0Ek3nLHew?rel=0 ->
// https://img.youtube.com/vi/ts0Ek3nLHew/0.jpg
if (url.startsWith("https://www.youtube.com/embed/")) {
url = "https://img.youtube.com/vi/" + url.substring(30, url.length()-6) + "/0.jpg";
}
this.url = url;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public String getCopyright() {
return copyright;
}
public void setCopyright(String copyright) {
this.copyright = copyright;
}
}

View File

@@ -0,0 +1,8 @@
package iotd;
import java.util.ArrayList;
public interface CacheService {
Image getImage();
void putImage(Image img);
}

View File

@@ -0,0 +1,34 @@
package iotd;
import java.util.concurrent.TimeUnit;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.CacheManager;
import org.ehcache.Cache;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;
import org.springframework.stereotype.Service;
@Service("CacheService")
public class MemoryCacheService implements CacheService {
private static Cache<String, Image> _ImageCache;
public MemoryCacheService() {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().withCache("ImageCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,Image.class,
ResourcePoolsBuilder.heap(100))
.withExpiry(Expirations.timeToLiveExpiration(new Duration(2, TimeUnit.HOURS)))
.build()).build(true);
_ImageCache = cacheManager.getCache("ImageCache", String.class, Image.class);
}
public Image getImage(){
return (Image) _ImageCache.get("_Image");
}
public void putImage(Image img){
_ImageCache.put("_Image", img);
}
}

View File

@@ -0,0 +1,4 @@
server.port=80
apod.url=https://api.nasa.gov/planetary/apod?api_key=
apod.key=DEMO_KEY
management.endpoints.web.exposure.include=health,info,prometheus

View File

@@ -0,0 +1,10 @@
$images=$(yq e '.services.[].image' docker-compose.yml)
foreach ($image in $images)
{
docker manifest create --amend $image `
"$($image)-linux-arm64" `
"$($image)-linux-amd64"
docker manifest push $image
}